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 = "Vec::is_empty")]
614 pub children: Vec<WidgetElement>,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub spacing: Option<f64>,
618 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
620 pub font_size: Option<f64>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub color: Option<ColorValue>,
624 #[serde(flatten)]
625 pub style: ElementStyle,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize, Default)]
631#[cfg_attr(feature = "schema", derive(JsonSchema))]
632#[serde(rename_all = "camelCase")]
633pub struct LinkElement {
634 #[serde(default, skip_serializing_if = "Vec::is_empty")]
636 pub children: Vec<WidgetElement>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub url: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub action: Option<String>,
643 #[serde(flatten)]
644 pub style: ElementStyle,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize, Default)]
650#[cfg_attr(feature = "schema", derive(JsonSchema))]
651#[serde(rename_all = "camelCase")]
652pub struct ShapeElement {
653 #[serde(rename = "shapeType")]
655 pub shape_type: ShapeType,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub fill: Option<ColorValue>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub stroke: Option<ColorValue>,
662 #[serde(
664 rename = "strokeWidth",
665 default,
666 skip_serializing_if = "Option::is_none"
667 )]
668 pub stroke_width: Option<f64>,
669 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub size: Option<f64>,
672 #[serde(flatten)]
673 pub style: ElementStyle,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize, Default)]
679#[cfg_attr(feature = "schema", derive(JsonSchema))]
680#[serde(rename_all = "camelCase")]
681pub struct TimerElement {
682 #[serde(rename = "targetDate")]
684 pub target_date: String,
685 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub counting: Option<TimerCounting>,
688 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
690 pub font_size: Option<f64>,
691 #[serde(
693 rename = "fontWeight",
694 default,
695 skip_serializing_if = "Option::is_none"
696 )]
697 pub font_weight: Option<FontWeight>,
698 #[serde(default, skip_serializing_if = "Option::is_none")]
700 pub color: Option<ColorValue>,
701 #[serde(flatten)]
702 pub style: ElementStyle,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize, Default)]
708#[cfg_attr(feature = "schema", derive(JsonSchema))]
709#[serde(rename_all = "camelCase")]
710pub struct CanvasElement {
711 pub width: f64,
713 pub height: f64,
715 #[serde(default, skip_serializing_if = "Vec::is_empty")]
717 pub elements: Vec<CanvasDrawCommand>,
718 #[serde(flatten)]
719 pub style: ElementStyle,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize, Default)]
725#[cfg_attr(feature = "schema", derive(JsonSchema))]
726#[serde(rename_all = "camelCase")]
727pub struct LabelElement {
728 pub text: String,
730 #[serde(rename = "systemName")]
732 pub system_name: String,
733 #[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
735 pub icon_color: Option<ColorValue>,
736 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
738 pub font_size: Option<f64>,
739 #[serde(
741 rename = "fontWeight",
742 default,
743 skip_serializing_if = "Option::is_none"
744 )]
745 pub font_weight: Option<FontWeight>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
748 pub color: Option<ColorValue>,
749 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub spacing: Option<f64>,
752 #[serde(flatten)]
753 pub style: ElementStyle,
755}
756
757impl From<VStackElement> for WidgetElement {
758 fn from(value: VStackElement) -> Self {
759 WidgetElement::VStack(value)
760 }
761}
762
763impl From<HStackElement> for WidgetElement {
764 fn from(value: HStackElement) -> Self {
765 WidgetElement::HStack(value)
766 }
767}
768
769impl From<ZStackElement> for WidgetElement {
770 fn from(value: ZStackElement) -> Self {
771 WidgetElement::ZStack(value)
772 }
773}
774
775impl From<GridElement> for WidgetElement {
776 fn from(value: GridElement) -> Self {
777 WidgetElement::Grid(value)
778 }
779}
780
781impl From<ContainerElement> for WidgetElement {
782 fn from(value: ContainerElement) -> Self {
783 WidgetElement::Container(value)
784 }
785}
786
787impl From<TextElement> for WidgetElement {
788 fn from(value: TextElement) -> Self {
789 WidgetElement::Text(value)
790 }
791}
792
793impl From<ImageElement> for WidgetElement {
794 fn from(value: ImageElement) -> Self {
795 WidgetElement::Image(value)
796 }
797}
798
799impl From<ProgressElement> for WidgetElement {
800 fn from(value: ProgressElement) -> Self {
801 WidgetElement::Progress(value)
802 }
803}
804
805impl From<GaugeElement> for WidgetElement {
806 fn from(value: GaugeElement) -> Self {
807 WidgetElement::Gauge(value)
808 }
809}
810
811impl From<ButtonElement> for WidgetElement {
812 fn from(value: ButtonElement) -> Self {
813 WidgetElement::Button(value)
814 }
815}
816
817impl From<ToggleElement> for WidgetElement {
818 fn from(value: ToggleElement) -> Self {
819 WidgetElement::Toggle(value)
820 }
821}
822
823impl From<DividerElement> for WidgetElement {
824 fn from(value: DividerElement) -> Self {
825 WidgetElement::Divider(value)
826 }
827}
828
829impl From<SpacerElement> for WidgetElement {
830 fn from(value: SpacerElement) -> Self {
831 WidgetElement::Spacer(value)
832 }
833}
834
835impl From<DateElement> for WidgetElement {
836 fn from(value: DateElement) -> Self {
837 WidgetElement::Date(value)
838 }
839}
840
841impl From<ChartElement> for WidgetElement {
842 fn from(value: ChartElement) -> Self {
843 WidgetElement::Chart(value)
844 }
845}
846
847impl From<ListElement> for WidgetElement {
848 fn from(value: ListElement) -> Self {
849 WidgetElement::List(value)
850 }
851}
852
853impl From<LinkElement> for WidgetElement {
854 fn from(value: LinkElement) -> Self {
855 WidgetElement::Link(value)
856 }
857}
858
859impl From<ShapeElement> for WidgetElement {
860 fn from(value: ShapeElement) -> Self {
861 WidgetElement::Shape(value)
862 }
863}
864
865impl From<TimerElement> for WidgetElement {
866 fn from(value: TimerElement) -> Self {
867 WidgetElement::Timer(value)
868 }
869}
870
871impl From<CanvasElement> for WidgetElement {
872 fn from(value: CanvasElement) -> Self {
873 WidgetElement::Canvas(value)
874 }
875}
876
877impl From<LabelElement> for WidgetElement {
878 fn from(value: LabelElement) -> Self {
879 WidgetElement::Label(value)
880 }
881}
882
883pub fn text(content: impl Into<String>) -> TextElement {
885 TextElement {
886 content: content.into(),
887 ..Default::default()
888 }
889}
890
891pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
893 VStackElement {
894 children,
895 ..Default::default()
896 }
897}
898
899pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
901 HStackElement {
902 children,
903 ..Default::default()
904 }
905}
906
907fn default_columns() -> u32 {
908 2
909}
910fn default_total() -> f64 {
911 1.0
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use std::path::PathBuf;
918
919 #[test]
920 fn option_none_omitted_from_json() {
921 let cfg = WidgetConfig::small(TextElement {
922 content: "hi".into(),
923 font_size: Some(12.0),
924 ..Default::default()
925 });
926 let json = serde_json::to_string(&cfg).unwrap();
927 assert!(!json.contains("null"), "JSON must not contain null: {json}");
928 assert!(!json.contains("\"medium\""));
929 assert!(!json.contains("\"fontWeight\""));
930 let back: WidgetConfig = serde_json::from_str(&json).unwrap();
931 assert!(back.medium.is_none());
932 match back.small.unwrap() {
933 WidgetElement::Text(TextElement {
934 font_weight,
935 content,
936 ..
937 }) => {
938 assert!(font_weight.is_none());
939 assert_eq!(content, "hi");
940 }
941 other => panic!("expected text, got {other:?}"),
942 }
943 }
944
945 #[test]
946 fn ergonomic_builders_serialize() {
947 let cfg = WidgetConfig::small(vstack(vec![text("72°")
948 .font_size(36.0)
949 .font_weight(FontWeight::Bold)
950 .color("#fff")
951 .into()]));
952 let v = serde_json::to_value(&cfg).unwrap();
953 assert_eq!(v["small"]["type"], "vstack");
954 assert_eq!(v["small"]["children"][0]["type"], "text");
955 assert_eq!(v["small"]["children"][0]["content"], "72°");
956 assert_eq!(v["small"]["children"][0]["fontSize"], 36.0);
957 assert_eq!(v["small"]["children"][0]["fontWeight"], "bold");
958 assert_eq!(v["small"]["children"][0]["color"], "#fff");
959 }
960
961 fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
962 let Ok(entries) = std::fs::read_dir(dir) else {
963 return;
964 };
965 for entry in entries.flatten() {
966 let path = entry.path();
967 if path.is_dir() {
968 walk_json_files(&path, out);
969 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
970 out.push(path);
971 }
972 }
973 }
974
975 #[test]
978 fn wire_format_unchanged() {
979 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
980 let mut files = Vec::new();
981 walk_json_files(&root, &mut files);
982 assert!(
983 !files.is_empty(),
984 "expected fixtures under {}",
985 root.display()
986 );
987 for path in files {
988 let raw = std::fs::read_to_string(&path).unwrap();
989 let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
990 panic!("deserialize {}: {e}", path.display());
991 });
992 let encoded = serde_json::to_value(&cfg).unwrap();
993 let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
994 assert_eq!(
995 encoded,
996 serde_json::to_value(&again).unwrap(),
997 "wire drift in {}",
998 path.display()
999 );
1000 let out = serde_json::to_string(&cfg).unwrap();
1001 assert!(
1002 !out.contains("null"),
1003 "{} serialized with null: {out}",
1004 path.display()
1005 );
1006 }
1007 }
1008}