1mod activity;
8mod data;
9mod style;
10pub use activity::*;
11pub use data::*;
12pub use style::*;
13
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17#[cfg(feature = "schema")]
18use schemars::JsonSchema;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
27#[cfg_attr(feature = "schema", derive(JsonSchema))]
28#[serde(rename_all = "camelCase")]
29pub struct WidgetWindowConfig {
30 pub label: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub url: Option<String>,
36 pub width: f64,
38 pub height: f64,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub x: Option<f64>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub y: Option<f64>,
46 #[serde(default)]
48 pub always_on_top: bool,
49 #[serde(default = "default_true")]
51 pub skip_taskbar: bool,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub group: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub widget_id: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub instance: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub size: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub layer: Option<WindowLayer>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub click_through: Option<bool>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub focusable: Option<bool>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub all_workspaces: Option<bool>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub shadow: Option<bool>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub effects: Option<WindowEffectHint>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub placement: Option<WidgetPlacement>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub remember_position: Option<bool>,
95}
96
97impl Default for WidgetWindowConfig {
98 fn default() -> Self {
99 Self {
100 label: String::new(),
101 url: None,
102 width: 0.0,
103 height: 0.0,
104 x: None,
105 y: None,
106 always_on_top: false,
107 skip_taskbar: true,
108 group: None,
109 widget_id: None,
110 instance: None,
111 size: None,
112 layer: None,
113 click_through: None,
114 focusable: None,
115 all_workspaces: None,
116 shadow: None,
117 effects: None,
118 placement: None,
119 remember_position: None,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[cfg_attr(feature = "schema", derive(JsonSchema))]
127#[serde(rename_all = "camelCase")]
128pub enum WindowLayer {
129 Normal,
131 Top,
133 Bottom,
135 Desktop,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143#[cfg_attr(feature = "schema", derive(JsonSchema))]
144#[serde(rename_all = "camelCase")]
145pub enum WindowEffectHint {
146 Vibrancy,
148 Mica,
150 Acrylic,
152 Blur,
154}
155
156#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(JsonSchema))]
159#[serde(rename_all = "camelCase")]
160pub struct WidgetPlacement {
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub monitor: Option<MonitorSelector>,
164 #[serde(default)]
166 pub anchor: PlacementAnchor,
167 #[serde(default)]
170 pub offset_x: f64,
171 #[serde(default)]
173 pub offset_y: f64,
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[cfg_attr(feature = "schema", derive(JsonSchema))]
179#[serde(untagged)]
180pub enum MonitorSelector {
181 Index(u32),
183 Name(String),
185}
186
187#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
189#[cfg_attr(feature = "schema", derive(JsonSchema))]
190#[serde(rename_all = "kebab-case")]
191pub enum PlacementAnchor {
192 #[default]
194 TopLeft,
195 Top,
197 TopRight,
199 Left,
201 Center,
203 Right,
205 BottomLeft,
207 Bottom,
209 BottomRight,
211}
212
213fn default_true() -> bool {
214 true
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "schema", derive(JsonSchema))]
222#[serde(rename_all = "camelCase")]
223pub struct WidgetConfig {
224 #[serde(default = "default_version")]
226 pub version: u32,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub small: Option<WidgetElement>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub medium: Option<WidgetElement>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub large: Option<WidgetElement>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub accessory: Option<WidgetElement>,
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
245 pub entries: Vec<WidgetEntry>,
246 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
248 pub strings: BTreeMap<String, BTreeMap<String, String>>,
249 #[serde(
251 rename = "defaultLocale",
252 default,
253 skip_serializing_if = "Option::is_none"
254 )]
255 pub default_locale: Option<String>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub source: Option<DataSource>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 #[cfg_attr(
262 feature = "schema",
263 schemars(with = "Option<BTreeMap<String, serde_json::Value>>")
264 )]
265 pub data: Option<serde_json::Value>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
272 #[cfg_attr(
273 feature = "schema",
274 schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
275 )]
276 pub state: Option<serde_json::Map<String, serde_json::Value>>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub transition: Option<Transition>,
282 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
284 pub templates: BTreeMap<String, WidgetElement>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub requires: Option<Requires>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub fallback: Option<FallbackLayouts>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, Default)]
295#[cfg_attr(feature = "schema", derive(JsonSchema))]
296#[serde(rename_all = "camelCase")]
297pub struct Requires {
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub resolve: Option<u64>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, Default)]
305#[cfg_attr(feature = "schema", derive(JsonSchema))]
306#[serde(rename_all = "camelCase")]
307pub struct FallbackLayouts {
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub small: Option<WidgetElement>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub medium: Option<WidgetElement>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub large: Option<WidgetElement>,
317}
318
319impl Default for WidgetConfig {
320 fn default() -> Self {
321 Self {
322 version: 1,
323 small: None,
324 medium: None,
325 large: None,
326 accessory: None,
327 entries: Vec::new(),
328 strings: BTreeMap::new(),
329 default_locale: None,
330 source: None,
331 data: None,
332 state: None,
333 transition: None,
334 templates: BTreeMap::new(),
335 requires: None,
336 fallback: None,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, Default)]
343#[cfg_attr(feature = "schema", derive(JsonSchema))]
344#[serde(rename_all = "camelCase")]
345pub struct WidgetEntry {
346 pub at: u64,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub small: Option<WidgetElement>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub medium: Option<WidgetElement>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub large: Option<WidgetElement>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub accessory: Option<WidgetElement>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
367#[cfg_attr(feature = "schema", derive(JsonSchema))]
368#[serde(rename_all = "camelCase")]
369pub struct DataSource {
370 pub url: String,
372 #[serde(
374 rename = "refreshMinutes",
375 default,
376 skip_serializing_if = "Option::is_none"
377 )]
378 pub refresh_minutes: Option<u32>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub method: Option<String>,
382 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
384 pub headers: BTreeMap<String, String>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub body: Option<String>,
388}
389
390impl WidgetConfig {
391 pub fn small(el: impl Into<WidgetElement>) -> Self {
393 Self {
394 small: Some(el.into()),
395 ..Default::default()
396 }
397 }
398
399 pub fn layouts_mut(&mut self) -> impl Iterator<Item = &mut WidgetElement> {
402 let fallback = self
403 .fallback
404 .as_mut()
405 .map(|f| [&mut f.small, &mut f.medium, &mut f.large]);
406 [
407 &mut self.small,
408 &mut self.medium,
409 &mut self.large,
410 &mut self.accessory,
411 ]
412 .into_iter()
413 .chain(
414 self.entries
415 .iter_mut()
416 .flat_map(|e| [&mut e.small, &mut e.medium, &mut e.large, &mut e.accessory]),
417 )
418 .chain(fallback.into_iter().flatten())
419 .flatten()
420 .chain(self.templates.values_mut())
421 }
422
423 pub fn with_medium(mut self, el: impl Into<WidgetElement>) -> Self {
425 self.medium = Some(el.into());
426 self
427 }
428
429 pub fn with_large(mut self, el: impl Into<WidgetElement>) -> Self {
431 self.large = Some(el.into());
432 self
433 }
434}
435
436fn default_version() -> u32 {
437 1
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442#[cfg_attr(feature = "schema", derive(JsonSchema))]
443#[serde(tag = "type", rename_all = "camelCase")]
444pub enum WidgetElement {
445 #[serde(rename = "vstack")]
447 VStack(VStackElement),
448 #[serde(rename = "hstack")]
450 HStack(HStackElement),
451 #[serde(rename = "zstack")]
453 ZStack(ZStackElement),
454 #[serde(rename = "grid")]
456 Grid(GridElement),
457 #[serde(rename = "container")]
459 Container(ContainerElement),
460 #[serde(rename = "text")]
462 Text(TextElement),
463 #[serde(rename = "image")]
465 Image(ImageElement),
466 #[serde(rename = "progress")]
468 Progress(ProgressElement),
469 #[serde(rename = "gauge")]
471 Gauge(GaugeElement),
472 #[serde(rename = "button")]
474 Button(ButtonElement),
475 #[serde(rename = "toggle")]
477 Toggle(ToggleElement),
478 #[serde(rename = "divider")]
480 Divider(DividerElement),
481 #[serde(rename = "spacer")]
483 Spacer(SpacerElement),
484 #[serde(rename = "date")]
486 Date(DateElement),
487 #[serde(rename = "chart")]
489 Chart(ChartElement),
490 #[serde(rename = "list")]
492 List(ListElement),
493 #[serde(rename = "link")]
495 Link(LinkElement),
496 #[serde(rename = "shape")]
498 Shape(ShapeElement),
499 #[serde(rename = "timer")]
501 Timer(TimerElement),
502 #[serde(rename = "canvas")]
504 Canvas(CanvasElement),
505 #[serde(rename = "label")]
507 Label(LabelElement),
508 #[serde(rename = "repeat")]
511 Repeat(RepeatElement),
512 #[serde(rename = "use")]
515 Use(UseElement),
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg_attr(feature = "schema", derive(JsonSchema))]
521#[serde(rename_all = "camelCase")]
522pub struct RepeatElement {
523 pub each: String,
525 #[serde(rename = "as", default, skip_serializing_if = "Option::is_none")]
527 pub as_name: Option<String>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub limit: Option<u32>,
531 pub template: Box<WidgetElement>,
533 #[serde(flatten)]
534 pub style: ElementStyle,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540#[cfg_attr(feature = "schema", derive(JsonSchema))]
541#[serde(rename_all = "camelCase")]
542pub struct UseElement {
543 pub template: String,
545 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
548 #[cfg_attr(
549 feature = "schema",
550 schemars(with = "BTreeMap<String, serde_json::Value>")
551 )]
552 pub args: BTreeMap<String, serde_json::Value>,
553 #[serde(flatten)]
554 pub style: ElementStyle,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, Default)]
560#[cfg_attr(feature = "schema", derive(JsonSchema))]
561#[serde(rename_all = "camelCase")]
562pub struct VStackElement {
563 #[serde(default, skip_serializing_if = "Vec::is_empty")]
565 pub children: Vec<WidgetElement>,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub spacing: Option<f64>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub alignment: Option<HorizontalAlignment>,
572 #[serde(rename = "justify", default, skip_serializing_if = "Option::is_none")]
574 pub justify: Option<Justify>,
575 #[serde(flatten)]
576 pub style: ElementStyle,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, Default)]
582#[cfg_attr(feature = "schema", derive(JsonSchema))]
583#[serde(rename_all = "camelCase")]
584pub struct HStackElement {
585 #[serde(default, skip_serializing_if = "Vec::is_empty")]
587 pub children: Vec<WidgetElement>,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
590 pub spacing: Option<f64>,
591 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub alignment: Option<VerticalAlignment>,
594 #[serde(rename = "justify", default, skip_serializing_if = "Option::is_none")]
596 pub justify: Option<Justify>,
597 #[serde(flatten)]
598 pub style: ElementStyle,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize, Default)]
604#[cfg_attr(feature = "schema", derive(JsonSchema))]
605#[serde(rename_all = "camelCase")]
606pub struct ZStackElement {
607 #[serde(default, skip_serializing_if = "Vec::is_empty")]
609 pub children: Vec<WidgetElement>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub alignment: Option<String>,
613 #[serde(flatten)]
614 pub style: ElementStyle,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize, Default)]
620#[cfg_attr(feature = "schema", derive(JsonSchema))]
621#[serde(rename_all = "camelCase")]
622pub struct GridElement {
623 #[serde(default, skip_serializing_if = "Vec::is_empty")]
625 pub children: Vec<WidgetElement>,
626 #[serde(default = "default_columns")]
628 pub columns: u32,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub spacing: Option<f64>,
632 #[serde(
634 rename = "rowSpacing",
635 default,
636 skip_serializing_if = "Option::is_none"
637 )]
638 pub row_spacing: Option<f64>,
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 ContainerElement {
649 #[serde(default, skip_serializing_if = "Vec::is_empty")]
651 pub children: Vec<WidgetElement>,
652 #[serde(
654 rename = "contentAlignment",
655 default,
656 skip_serializing_if = "Option::is_none"
657 )]
658 pub content_alignment: Option<String>,
659 #[serde(flatten)]
660 pub style: ElementStyle,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, Default)]
666#[cfg_attr(feature = "schema", derive(JsonSchema))]
667#[serde(rename_all = "camelCase")]
668pub struct TextElement {
669 pub content: String,
671 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
673 pub font_size: Option<f64>,
674 #[serde(
676 rename = "fontWeight",
677 default,
678 skip_serializing_if = "Option::is_none"
679 )]
680 pub font_weight: Option<FontWeight>,
681 #[serde(
683 rename = "fontDesign",
684 default,
685 skip_serializing_if = "Option::is_none"
686 )]
687 pub font_design: Option<FontDesign>,
688 #[serde(rename = "textStyle", default, skip_serializing_if = "Option::is_none")]
691 pub text_style: Option<TextStyle>,
692 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub color: Option<ColorValue>,
695 #[serde(default, skip_serializing_if = "Option::is_none")]
697 pub alignment: Option<TextAlignment>,
698 #[serde(rename = "lineLimit", default, skip_serializing_if = "Option::is_none")]
700 pub line_limit: Option<u32>,
701 #[serde(
703 rename = "truncationMode",
704 default,
705 skip_serializing_if = "Option::is_none"
706 )]
707 pub truncation_mode: Option<TruncationMode>,
708 #[serde(
710 rename = "minimumScaleFactor",
711 default,
712 skip_serializing_if = "Option::is_none"
713 )]
714 pub minimum_scale_factor: Option<f64>,
715 #[serde(
717 rename = "lineHeight",
718 default,
719 skip_serializing_if = "Option::is_none"
720 )]
721 pub line_height: Option<f64>,
722 #[serde(
724 rename = "letterSpacing",
725 default,
726 skip_serializing_if = "Option::is_none"
727 )]
728 pub letter_spacing: Option<f64>,
729 #[serde(rename = "underline", default, skip_serializing_if = "Option::is_none")]
731 pub underline: Option<bool>,
732 #[serde(
734 rename = "strikethrough",
735 default,
736 skip_serializing_if = "Option::is_none"
737 )]
738 pub strikethrough: Option<bool>,
739 #[serde(rename = "italic", default, skip_serializing_if = "Option::is_none")]
741 pub italic: Option<bool>,
742 #[serde(
744 rename = "monospacedDigit",
745 default,
746 skip_serializing_if = "Option::is_none"
747 )]
748 pub monospaced_digit: Option<bool>,
749 #[serde(flatten)]
750 pub style: ElementStyle,
752}
753
754impl TextElement {
755 pub fn font_size(mut self, size: f64) -> Self {
757 self.font_size = Some(size);
758 self
759 }
760
761 pub fn font_weight(mut self, weight: FontWeight) -> Self {
763 self.font_weight = Some(weight);
764 self
765 }
766
767 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
769 self.color = Some(color.into());
770 self
771 }
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize, Default)]
776#[cfg_attr(feature = "schema", derive(JsonSchema))]
777#[serde(rename_all = "camelCase")]
778pub struct ImageElement {
779 #[serde(
781 rename = "systemName",
782 default,
783 skip_serializing_if = "Option::is_none"
784 )]
785 pub system_name: Option<String>,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub data: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub url: Option<String>,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
794 pub size: Option<f64>,
795 #[serde(default, skip_serializing_if = "Option::is_none")]
797 pub color: Option<ColorValue>,
798 #[serde(
800 rename = "contentMode",
801 default,
802 skip_serializing_if = "Option::is_none"
803 )]
804 pub content_mode: Option<ContentMode>,
805 #[serde(
807 rename = "fallbackSystemName",
808 default,
809 skip_serializing_if = "Option::is_none"
810 )]
811 pub fallback_system_name: Option<String>,
812 #[serde(
814 rename = "symbolRendering",
815 default,
816 skip_serializing_if = "Option::is_none"
817 )]
818 pub symbol_rendering: Option<SymbolRendering>,
819 #[serde(
821 rename = "symbolWeight",
822 default,
823 skip_serializing_if = "Option::is_none"
824 )]
825 pub symbol_weight: Option<FontWeight>,
826 #[serde(
828 rename = "paletteColors",
829 default,
830 skip_serializing_if = "Vec::is_empty"
831 )]
832 pub palette_colors: Vec<ColorValue>,
833 #[serde(flatten)]
834 pub style: ElementStyle,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize)]
840#[cfg_attr(feature = "schema", derive(JsonSchema))]
841#[serde(rename_all = "camelCase")]
842pub struct ProgressElement {
843 pub value: f64,
845 #[serde(default = "default_total")]
847 pub total: f64,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub label: Option<String>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub tint: Option<ColorValue>,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub color: Option<ColorValue>,
857 #[serde(rename = "barStyle", default, skip_serializing_if = "Option::is_none")]
859 pub bar_style: Option<ProgressStyle>,
860 #[serde(rename = "startAt", default, skip_serializing_if = "Option::is_none")]
864 pub start_at: Option<u64>,
865 #[serde(rename = "endAt", default, skip_serializing_if = "Option::is_none")]
867 pub end_at: Option<u64>,
868 #[serde(
870 rename = "trackColor",
871 default,
872 skip_serializing_if = "Option::is_none"
873 )]
874 pub track_color: Option<ColorValue>,
875 #[serde(rename = "thickness", default, skip_serializing_if = "Option::is_none")]
877 pub thickness: Option<f64>,
878 #[serde(flatten)]
879 pub style: ElementStyle,
881}
882
883impl Default for ProgressElement {
884 fn default() -> Self {
885 Self {
886 value: 0.0,
887 total: 1.0,
888 label: None,
889 tint: None,
890 color: None,
891 bar_style: None,
892 start_at: None,
893 end_at: None,
894 track_color: None,
895 thickness: None,
896 style: ElementStyle::default(),
897 }
898 }
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize, Default)]
903#[cfg_attr(feature = "schema", derive(JsonSchema))]
904#[serde(rename_all = "camelCase")]
905pub struct GaugeElement {
906 pub value: f64,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub min: Option<f64>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub max: Option<f64>,
914 #[serde(default, skip_serializing_if = "Option::is_none")]
916 pub label: Option<String>,
917 #[serde(
919 rename = "currentValueLabel",
920 default,
921 skip_serializing_if = "Option::is_none"
922 )]
923 pub current_value_label: Option<String>,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
926 pub tint: Option<ColorValue>,
927 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub color: Option<ColorValue>,
930 #[serde(
932 rename = "gaugeStyle",
933 default,
934 skip_serializing_if = "Option::is_none"
935 )]
936 pub gauge_style: Option<GaugeStyle>,
937 #[serde(rename = "startAt", default, skip_serializing_if = "Option::is_none")]
939 pub start_at: Option<u64>,
940 #[serde(rename = "endAt", default, skip_serializing_if = "Option::is_none")]
942 pub end_at: Option<u64>,
943 #[serde(rename = "minLabel", default, skip_serializing_if = "Option::is_none")]
945 pub min_label: Option<String>,
946 #[serde(rename = "maxLabel", default, skip_serializing_if = "Option::is_none")]
948 pub max_label: Option<String>,
949 #[serde(flatten)]
950 pub style: ElementStyle,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize, Default)]
956#[cfg_attr(feature = "schema", derive(JsonSchema))]
957#[serde(rename_all = "camelCase")]
958pub struct ButtonElement {
959 pub label: String,
961 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub url: Option<String>,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
966 pub action: Option<String>,
967 #[serde(default, skip_serializing_if = "Option::is_none")]
972 #[cfg_attr(
973 feature = "schema",
974 schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
975 )]
976 pub set: Option<serde_json::Map<String, serde_json::Value>>,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub color: Option<ColorValue>,
980 #[serde(
982 rename = "backgroundColor",
983 default,
984 skip_serializing_if = "Option::is_none"
985 )]
986 pub background_color: Option<ColorValue>,
987 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
989 pub font_size: Option<f64>,
990 #[serde(
992 rename = "textAlignment",
993 default,
994 skip_serializing_if = "Option::is_none"
995 )]
996 pub text_alignment: Option<TextAlignment>,
997 #[serde(rename = "variant", default, skip_serializing_if = "Option::is_none")]
1000 pub variant: Option<ButtonVariant>,
1001 #[serde(
1003 rename = "systemName",
1004 default,
1005 skip_serializing_if = "Option::is_none"
1006 )]
1007 pub system_name: Option<String>,
1008 #[serde(rename = "disabled", default, skip_serializing_if = "Option::is_none")]
1010 pub disabled: Option<bool>,
1011 #[serde(flatten)]
1012 pub style: ElementStyle,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1018#[cfg_attr(feature = "schema", derive(JsonSchema))]
1019#[serde(rename_all = "camelCase")]
1020pub struct ToggleElement {
1021 #[serde(rename = "isOn")]
1023 pub is_on: bool,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub label: Option<String>,
1027 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub tint: Option<ColorValue>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 pub action: Option<String>,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1038 #[cfg_attr(
1039 feature = "schema",
1040 schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
1041 )]
1042 pub set: Option<serde_json::Map<String, serde_json::Value>>,
1043 #[serde(rename = "variant", default, skip_serializing_if = "Option::is_none")]
1046 pub variant: Option<ToggleVariant>,
1047 #[serde(
1049 rename = "uncheckedTint",
1050 default,
1051 skip_serializing_if = "Option::is_none"
1052 )]
1053 pub unchecked_tint: Option<ColorValue>,
1054 #[serde(flatten)]
1055 pub style: ElementStyle,
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1061#[cfg_attr(feature = "schema", derive(JsonSchema))]
1062#[serde(rename_all = "camelCase")]
1063pub struct DividerElement {
1064 #[serde(default, skip_serializing_if = "Option::is_none")]
1066 pub color: Option<ColorValue>,
1067 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub thickness: Option<f64>,
1070 #[serde(flatten)]
1071 pub style: ElementStyle,
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1077#[cfg_attr(feature = "schema", derive(JsonSchema))]
1078#[serde(rename_all = "camelCase")]
1079pub struct SpacerElement {
1080 #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
1082 pub min_length: Option<f64>,
1083}
1084
1085#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1087#[cfg_attr(feature = "schema", derive(JsonSchema))]
1088#[serde(rename_all = "camelCase")]
1089pub struct DateElement {
1090 pub date: String,
1092 #[serde(rename = "dateStyle", default, skip_serializing_if = "Option::is_none")]
1094 pub date_style: Option<DateStyle>,
1095 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
1097 pub font_size: Option<f64>,
1098 #[serde(default, skip_serializing_if = "Option::is_none")]
1100 pub color: Option<ColorValue>,
1101 #[serde(flatten)]
1102 pub style: ElementStyle,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1108#[cfg_attr(feature = "schema", derive(JsonSchema))]
1109#[serde(rename_all = "camelCase")]
1110pub struct ChartElement {
1111 #[serde(rename = "chartType")]
1113 pub chart_type: ChartType,
1114 #[serde(default, rename = "chartData", skip_serializing_if = "Vec::is_empty")]
1116 pub chart_data: Vec<ChartDataPoint>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub tint: Option<ColorValue>,
1120 #[serde(rename = "showXAxis", default, skip_serializing_if = "Option::is_none")]
1123 pub show_x_axis: Option<bool>,
1124 #[serde(rename = "showYAxis", default, skip_serializing_if = "Option::is_none")]
1126 pub show_y_axis: Option<bool>,
1127 #[serde(
1129 rename = "showLegend",
1130 default,
1131 skip_serializing_if = "Option::is_none"
1132 )]
1133 pub show_legend: Option<bool>,
1134 #[serde(rename = "yMin", default, skip_serializing_if = "Option::is_none")]
1136 pub y_min: Option<f64>,
1137 #[serde(rename = "yMax", default, skip_serializing_if = "Option::is_none")]
1139 pub y_max: Option<f64>,
1140 #[serde(
1142 rename = "interpolation",
1143 default,
1144 skip_serializing_if = "Option::is_none"
1145 )]
1146 pub interpolation: Option<ChartInterpolation>,
1147 #[serde(
1149 rename = "innerRadius",
1150 default,
1151 skip_serializing_if = "Option::is_none"
1152 )]
1153 pub inner_radius: Option<f64>,
1154 #[serde(flatten)]
1155 pub style: ElementStyle,
1157}
1158
1159#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1161#[cfg_attr(feature = "schema", derive(JsonSchema))]
1162#[serde(rename_all = "camelCase")]
1163pub struct ListElement {
1164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1166 pub items: Vec<ListItem>,
1167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1169 pub children: Vec<WidgetElement>,
1170 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub spacing: Option<f64>,
1173 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
1175 pub font_size: Option<f64>,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub color: Option<ColorValue>,
1179 #[serde(flatten)]
1180 pub style: ElementStyle,
1182}
1183
1184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1186#[cfg_attr(feature = "schema", derive(JsonSchema))]
1187#[serde(rename_all = "camelCase")]
1188pub struct LinkElement {
1189 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1191 pub children: Vec<WidgetElement>,
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub url: Option<String>,
1195 #[serde(default, skip_serializing_if = "Option::is_none")]
1197 pub action: Option<String>,
1198 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 #[cfg_attr(
1204 feature = "schema",
1205 schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
1206 )]
1207 pub set: Option<serde_json::Map<String, serde_json::Value>>,
1208 #[serde(flatten)]
1209 pub style: ElementStyle,
1211}
1212
1213#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1215#[cfg_attr(feature = "schema", derive(JsonSchema))]
1216#[serde(rename_all = "camelCase")]
1217pub struct ShapeElement {
1218 #[serde(rename = "shapeType")]
1220 pub shape_type: ShapeType,
1221 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub fill: Option<ColorValue>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 pub stroke: Option<ColorValue>,
1227 #[serde(
1229 rename = "strokeWidth",
1230 default,
1231 skip_serializing_if = "Option::is_none"
1232 )]
1233 pub stroke_width: Option<f64>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1236 pub size: Option<f64>,
1237 #[serde(flatten)]
1238 pub style: ElementStyle,
1240}
1241
1242#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1244#[cfg_attr(feature = "schema", derive(JsonSchema))]
1245#[serde(rename_all = "camelCase")]
1246pub struct TimerElement {
1247 #[serde(rename = "targetDate")]
1249 pub target_date: String,
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub counting: Option<TimerCounting>,
1253 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
1255 pub font_size: Option<f64>,
1256 #[serde(
1258 rename = "fontWeight",
1259 default,
1260 skip_serializing_if = "Option::is_none"
1261 )]
1262 pub font_weight: Option<FontWeight>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub color: Option<ColorValue>,
1266 #[serde(rename = "startDate", default, skip_serializing_if = "Option::is_none")]
1268 pub start_date: Option<String>,
1269 #[serde(rename = "showHours", default, skip_serializing_if = "Option::is_none")]
1271 pub show_hours: Option<bool>,
1272 #[serde(
1274 rename = "completedText",
1275 default,
1276 skip_serializing_if = "Option::is_none"
1277 )]
1278 pub completed_text: Option<String>,
1279 #[serde(flatten)]
1280 pub style: ElementStyle,
1282}
1283
1284#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1286#[cfg_attr(feature = "schema", derive(JsonSchema))]
1287#[serde(rename_all = "camelCase")]
1288pub struct CanvasElement {
1289 pub width: f64,
1291 pub height: f64,
1293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1295 pub elements: Vec<CanvasDrawCommand>,
1296 #[serde(flatten)]
1297 pub style: ElementStyle,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1303#[cfg_attr(feature = "schema", derive(JsonSchema))]
1304#[serde(rename_all = "camelCase")]
1305pub struct LabelElement {
1306 pub text: String,
1308 #[serde(rename = "systemName")]
1310 pub system_name: String,
1311 #[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
1313 pub icon_color: Option<ColorValue>,
1314 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
1316 pub font_size: Option<f64>,
1317 #[serde(
1319 rename = "fontWeight",
1320 default,
1321 skip_serializing_if = "Option::is_none"
1322 )]
1323 pub font_weight: Option<FontWeight>,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1326 pub color: Option<ColorValue>,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1329 pub spacing: Option<f64>,
1330 #[serde(flatten)]
1331 pub style: ElementStyle,
1333}
1334
1335impl From<VStackElement> for WidgetElement {
1336 fn from(value: VStackElement) -> Self {
1337 WidgetElement::VStack(value)
1338 }
1339}
1340
1341impl From<HStackElement> for WidgetElement {
1342 fn from(value: HStackElement) -> Self {
1343 WidgetElement::HStack(value)
1344 }
1345}
1346
1347impl From<ZStackElement> for WidgetElement {
1348 fn from(value: ZStackElement) -> Self {
1349 WidgetElement::ZStack(value)
1350 }
1351}
1352
1353impl From<GridElement> for WidgetElement {
1354 fn from(value: GridElement) -> Self {
1355 WidgetElement::Grid(value)
1356 }
1357}
1358
1359impl From<ContainerElement> for WidgetElement {
1360 fn from(value: ContainerElement) -> Self {
1361 WidgetElement::Container(value)
1362 }
1363}
1364
1365impl From<TextElement> for WidgetElement {
1366 fn from(value: TextElement) -> Self {
1367 WidgetElement::Text(value)
1368 }
1369}
1370
1371impl From<ImageElement> for WidgetElement {
1372 fn from(value: ImageElement) -> Self {
1373 WidgetElement::Image(value)
1374 }
1375}
1376
1377impl From<ProgressElement> for WidgetElement {
1378 fn from(value: ProgressElement) -> Self {
1379 WidgetElement::Progress(value)
1380 }
1381}
1382
1383impl From<GaugeElement> for WidgetElement {
1384 fn from(value: GaugeElement) -> Self {
1385 WidgetElement::Gauge(value)
1386 }
1387}
1388
1389impl From<ButtonElement> for WidgetElement {
1390 fn from(value: ButtonElement) -> Self {
1391 WidgetElement::Button(value)
1392 }
1393}
1394
1395impl From<ToggleElement> for WidgetElement {
1396 fn from(value: ToggleElement) -> Self {
1397 WidgetElement::Toggle(value)
1398 }
1399}
1400
1401impl From<DividerElement> for WidgetElement {
1402 fn from(value: DividerElement) -> Self {
1403 WidgetElement::Divider(value)
1404 }
1405}
1406
1407impl From<SpacerElement> for WidgetElement {
1408 fn from(value: SpacerElement) -> Self {
1409 WidgetElement::Spacer(value)
1410 }
1411}
1412
1413impl From<DateElement> for WidgetElement {
1414 fn from(value: DateElement) -> Self {
1415 WidgetElement::Date(value)
1416 }
1417}
1418
1419impl From<ChartElement> for WidgetElement {
1420 fn from(value: ChartElement) -> Self {
1421 WidgetElement::Chart(value)
1422 }
1423}
1424
1425impl From<ListElement> for WidgetElement {
1426 fn from(value: ListElement) -> Self {
1427 WidgetElement::List(value)
1428 }
1429}
1430
1431impl From<LinkElement> for WidgetElement {
1432 fn from(value: LinkElement) -> Self {
1433 WidgetElement::Link(value)
1434 }
1435}
1436
1437impl From<ShapeElement> for WidgetElement {
1438 fn from(value: ShapeElement) -> Self {
1439 WidgetElement::Shape(value)
1440 }
1441}
1442
1443impl From<TimerElement> for WidgetElement {
1444 fn from(value: TimerElement) -> Self {
1445 WidgetElement::Timer(value)
1446 }
1447}
1448
1449impl From<CanvasElement> for WidgetElement {
1450 fn from(value: CanvasElement) -> Self {
1451 WidgetElement::Canvas(value)
1452 }
1453}
1454
1455impl From<LabelElement> for WidgetElement {
1456 fn from(value: LabelElement) -> Self {
1457 WidgetElement::Label(value)
1458 }
1459}
1460
1461pub fn text(content: impl Into<String>) -> TextElement {
1463 TextElement {
1464 content: content.into(),
1465 ..Default::default()
1466 }
1467}
1468
1469pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
1471 VStackElement {
1472 children,
1473 ..Default::default()
1474 }
1475}
1476
1477pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
1479 HStackElement {
1480 children,
1481 ..Default::default()
1482 }
1483}
1484
1485fn default_columns() -> u32 {
1486 2
1487}
1488fn default_total() -> f64 {
1489 1.0
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494 use super::*;
1495 use std::path::PathBuf;
1496
1497 #[test]
1501 fn build_serialize_parse_and_walk_layouts() {
1502 let c = WidgetConfig::small(text("s"))
1504 .with_medium(text("m"))
1505 .with_large(hstack(vec![text("l").into()]));
1506 assert!(c.small.is_some() && c.medium.is_some());
1507 match c.large {
1508 Some(WidgetElement::HStack(h)) => assert_eq!(h.children.len(), 1),
1509 other => panic!("{other:?}"),
1510 }
1511 let cfg = WidgetConfig::small(vstack(vec![text("72°")
1512 .font_size(36.0)
1513 .font_weight(FontWeight::Bold)
1514 .color("#fff")
1515 .into()]));
1516 let v = serde_json::to_value(&cfg).unwrap();
1517 assert_eq!(v["small"]["type"], "vstack");
1518 let child = &v["small"]["children"][0];
1519 assert_eq!(child["type"], "text");
1520 assert_eq!(child["content"], "72°");
1521 assert_eq!(child["fontSize"], 36.0);
1522 assert_eq!(child["fontWeight"], "bold");
1523 assert_eq!(child["color"], "#fff");
1524
1525 let cfg = WidgetConfig::small(TextElement {
1527 content: "hi".into(),
1528 font_size: Some(12.0),
1529 ..Default::default()
1530 });
1531 let json = serde_json::to_string(&cfg).unwrap();
1532 assert!(!json.contains("null"), "JSON must not contain null: {json}");
1533 assert!(!json.contains("\"medium\""));
1534 assert!(!json.contains("\"fontWeight\""));
1535 let back: WidgetConfig = serde_json::from_str(&json).unwrap();
1536 assert!(back.medium.is_none());
1537 match back.small.unwrap() {
1538 WidgetElement::Text(TextElement {
1539 font_weight,
1540 content,
1541 ..
1542 }) => {
1543 assert!(font_weight.is_none());
1544 assert_eq!(content, "hi");
1545 }
1546 other => panic!("expected text, got {other:?}"),
1547 }
1548
1549 let parsed: WidgetConfig = serde_json::from_str(r#"{"small":{"type":"grid"}}"#).unwrap();
1551 assert_eq!(parsed.version, 1);
1552 match parsed.small {
1553 Some(WidgetElement::Grid(g)) => assert_eq!(g.columns, 2),
1554 other => panic!("{other:?}"),
1555 }
1556 let w: WidgetWindowConfig =
1557 serde_json::from_str(r#"{"label":"a","width":1,"height":1}"#).unwrap();
1558 assert!(w.skip_taskbar);
1559 assert!(!w.always_on_top);
1560
1561 let mut c: WidgetConfig = serde_json::from_value(serde_json::json!({
1563 "small": {"type": "spacer"},
1564 "entries": [{"at": 1, "medium": {"type": "spacer"}, "large": {"type": "spacer"}}]
1565 }))
1566 .unwrap();
1567 assert_eq!(c.layouts_mut().count(), 3);
1568 }
1569
1570 fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1571 let Ok(entries) = std::fs::read_dir(dir) else {
1572 return;
1573 };
1574 for entry in entries.flatten() {
1575 let path = entry.path();
1576 if path.is_dir() {
1577 walk_json_files(&path, out);
1578 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
1579 out.push(path);
1580 }
1581 }
1582 }
1583
1584 #[test]
1587 fn wire_format_unchanged() {
1588 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
1589 let mut files = Vec::new();
1590 walk_json_files(&root, &mut files);
1591 assert!(
1592 !files.is_empty(),
1593 "expected fixtures under {}",
1594 root.display()
1595 );
1596 for path in files {
1597 let raw = std::fs::read_to_string(&path).unwrap();
1598 let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
1599 panic!("deserialize {}: {e}", path.display());
1600 });
1601 let encoded = serde_json::to_value(&cfg).unwrap();
1602 let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
1603 assert_eq!(
1604 encoded,
1605 serde_json::to_value(&again).unwrap(),
1606 "wire drift in {}",
1607 path.display()
1608 );
1609 let out = serde_json::to_string(&cfg).unwrap();
1610 assert!(
1611 !out.contains("null"),
1612 "{} serialized with null: {out}",
1613 path.display()
1614 );
1615 }
1616 }
1617}