mod activity;
mod data;
mod style;
pub use activity::*;
pub use data::*;
pub use style::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct WidgetWindowConfig {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub width: f64,
pub height: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub y: Option<f64>,
#[serde(default)]
pub always_on_top: bool,
#[serde(default = "default_true")]
pub skip_taskbar: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub widget_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub layer: Option<WindowLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub click_through: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub focusable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub all_workspaces: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shadow: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effects: Option<WindowEffectHint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placement: Option<WidgetPlacement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remember_position: Option<bool>,
}
impl Default for WidgetWindowConfig {
fn default() -> Self {
Self {
label: String::new(),
url: None,
width: 0.0,
height: 0.0,
x: None,
y: None,
always_on_top: false,
skip_taskbar: true,
group: None,
widget_id: None,
instance: None,
size: None,
layer: None,
click_through: None,
focusable: None,
all_workspaces: None,
shadow: None,
effects: None,
placement: None,
remember_position: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum WindowLayer {
Normal,
Top,
Bottom,
Desktop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum WindowEffectHint {
Vibrancy,
Mica,
Acrylic,
Blur,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct WidgetPlacement {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub monitor: Option<MonitorSelector>,
#[serde(default)]
pub anchor: PlacementAnchor,
#[serde(default)]
pub offset_x: f64,
#[serde(default)]
pub offset_y: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum MonitorSelector {
Index(u32),
Name(String),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum PlacementAnchor {
#[default]
TopLeft,
Top,
TopRight,
Left,
Center,
Right,
BottomLeft,
Bottom,
BottomRight,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct WidgetConfig {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub small: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub medium: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub large: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessory: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entries: Vec<WidgetEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub strings: BTreeMap<String, BTreeMap<String, String>>,
#[serde(
rename = "defaultLocale",
default,
skip_serializing_if = "Option::is_none"
)]
pub default_locale: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<DataSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<BTreeMap<String, serde_json::Value>>")
)]
pub data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
)]
pub state: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transition: Option<Transition>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub templates: BTreeMap<String, WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requires: Option<Requires>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<FallbackLayouts>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct Requires {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolve: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct FallbackLayouts {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub small: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub medium: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub large: Option<WidgetElement>,
}
impl Default for WidgetConfig {
fn default() -> Self {
Self {
version: 1,
small: None,
medium: None,
large: None,
accessory: None,
entries: Vec::new(),
strings: BTreeMap::new(),
default_locale: None,
source: None,
data: None,
state: None,
transition: None,
templates: BTreeMap::new(),
requires: None,
fallback: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct WidgetEntry {
pub at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub small: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub medium: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub large: Option<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessory: Option<WidgetElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct DataSource {
pub url: String,
#[serde(
rename = "refreshMinutes",
default,
skip_serializing_if = "Option::is_none"
)]
pub refresh_minutes: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub headers: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
impl WidgetConfig {
pub fn small(el: impl Into<WidgetElement>) -> Self {
Self {
small: Some(el.into()),
..Default::default()
}
}
pub fn layouts_mut(&mut self) -> impl Iterator<Item = &mut WidgetElement> {
let fallback = self
.fallback
.as_mut()
.map(|f| [&mut f.small, &mut f.medium, &mut f.large]);
[
&mut self.small,
&mut self.medium,
&mut self.large,
&mut self.accessory,
]
.into_iter()
.chain(
self.entries
.iter_mut()
.flat_map(|e| [&mut e.small, &mut e.medium, &mut e.large, &mut e.accessory]),
)
.chain(fallback.into_iter().flatten())
.flatten()
.chain(self.templates.values_mut())
}
pub fn with_medium(mut self, el: impl Into<WidgetElement>) -> Self {
self.medium = Some(el.into());
self
}
pub fn with_large(mut self, el: impl Into<WidgetElement>) -> Self {
self.large = Some(el.into());
self
}
}
fn default_version() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WidgetElement {
#[serde(rename = "vstack")]
VStack(VStackElement),
#[serde(rename = "hstack")]
HStack(HStackElement),
#[serde(rename = "zstack")]
ZStack(ZStackElement),
#[serde(rename = "grid")]
Grid(GridElement),
#[serde(rename = "container")]
Container(ContainerElement),
#[serde(rename = "text")]
Text(TextElement),
#[serde(rename = "image")]
Image(ImageElement),
#[serde(rename = "progress")]
Progress(ProgressElement),
#[serde(rename = "gauge")]
Gauge(GaugeElement),
#[serde(rename = "button")]
Button(ButtonElement),
#[serde(rename = "toggle")]
Toggle(ToggleElement),
#[serde(rename = "divider")]
Divider(DividerElement),
#[serde(rename = "spacer")]
Spacer(SpacerElement),
#[serde(rename = "date")]
Date(DateElement),
#[serde(rename = "chart")]
Chart(ChartElement),
#[serde(rename = "list")]
List(ListElement),
#[serde(rename = "link")]
Link(LinkElement),
#[serde(rename = "shape")]
Shape(ShapeElement),
#[serde(rename = "timer")]
Timer(TimerElement),
#[serde(rename = "canvas")]
Canvas(CanvasElement),
#[serde(rename = "label")]
Label(LabelElement),
#[serde(rename = "repeat")]
Repeat(RepeatElement),
#[serde(rename = "use")]
Use(UseElement),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct RepeatElement {
pub each: String,
#[serde(rename = "as", default, skip_serializing_if = "Option::is_none")]
pub as_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
pub template: Box<WidgetElement>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct UseElement {
pub template: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[cfg_attr(
feature = "schema",
schemars(with = "BTreeMap<String, serde_json::Value>")
)]
pub args: BTreeMap<String, serde_json::Value>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct VStackElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<HorizontalAlignment>,
#[serde(rename = "justify", default, skip_serializing_if = "Option::is_none")]
pub justify: Option<Justify>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct HStackElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<VerticalAlignment>,
#[serde(rename = "justify", default, skip_serializing_if = "Option::is_none")]
pub justify: Option<Justify>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ZStackElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<String>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GridElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default = "default_columns")]
pub columns: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(
rename = "rowSpacing",
default,
skip_serializing_if = "Option::is_none"
)]
pub row_spacing: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ContainerElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(
rename = "contentAlignment",
default,
skip_serializing_if = "Option::is_none"
)]
pub content_alignment: Option<String>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct TextElement {
pub content: String,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(
rename = "fontWeight",
default,
skip_serializing_if = "Option::is_none"
)]
pub font_weight: Option<FontWeight>,
#[serde(
rename = "fontDesign",
default,
skip_serializing_if = "Option::is_none"
)]
pub font_design: Option<FontDesign>,
#[serde(rename = "textStyle", default, skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<TextAlignment>,
#[serde(rename = "lineLimit", default, skip_serializing_if = "Option::is_none")]
pub line_limit: Option<u32>,
#[serde(
rename = "truncationMode",
default,
skip_serializing_if = "Option::is_none"
)]
pub truncation_mode: Option<TruncationMode>,
#[serde(
rename = "minimumScaleFactor",
default,
skip_serializing_if = "Option::is_none"
)]
pub minimum_scale_factor: Option<f64>,
#[serde(
rename = "lineHeight",
default,
skip_serializing_if = "Option::is_none"
)]
pub line_height: Option<f64>,
#[serde(
rename = "letterSpacing",
default,
skip_serializing_if = "Option::is_none"
)]
pub letter_spacing: Option<f64>,
#[serde(rename = "underline", default, skip_serializing_if = "Option::is_none")]
pub underline: Option<bool>,
#[serde(
rename = "strikethrough",
default,
skip_serializing_if = "Option::is_none"
)]
pub strikethrough: Option<bool>,
#[serde(rename = "italic", default, skip_serializing_if = "Option::is_none")]
pub italic: Option<bool>,
#[serde(
rename = "monospacedDigit",
default,
skip_serializing_if = "Option::is_none"
)]
pub monospaced_digit: Option<bool>,
#[serde(flatten)]
pub style: ElementStyle,
}
impl TextElement {
pub fn font_size(mut self, size: f64) -> Self {
self.font_size = Some(size);
self
}
pub fn font_weight(mut self, weight: FontWeight) -> Self {
self.font_weight = Some(weight);
self
}
pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
self.color = Some(color.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ImageElement {
#[serde(
rename = "systemName",
default,
skip_serializing_if = "Option::is_none"
)]
pub system_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(
rename = "contentMode",
default,
skip_serializing_if = "Option::is_none"
)]
pub content_mode: Option<ContentMode>,
#[serde(
rename = "fallbackSystemName",
default,
skip_serializing_if = "Option::is_none"
)]
pub fallback_system_name: Option<String>,
#[serde(
rename = "symbolRendering",
default,
skip_serializing_if = "Option::is_none"
)]
pub symbol_rendering: Option<SymbolRendering>,
#[serde(
rename = "symbolWeight",
default,
skip_serializing_if = "Option::is_none"
)]
pub symbol_weight: Option<FontWeight>,
#[serde(
rename = "paletteColors",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub palette_colors: Vec<ColorValue>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ProgressElement {
pub value: f64,
#[serde(default = "default_total")]
pub total: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tint: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(rename = "barStyle", default, skip_serializing_if = "Option::is_none")]
pub bar_style: Option<ProgressStyle>,
#[serde(rename = "startAt", default, skip_serializing_if = "Option::is_none")]
pub start_at: Option<u64>,
#[serde(rename = "endAt", default, skip_serializing_if = "Option::is_none")]
pub end_at: Option<u64>,
#[serde(
rename = "trackColor",
default,
skip_serializing_if = "Option::is_none"
)]
pub track_color: Option<ColorValue>,
#[serde(rename = "thickness", default, skip_serializing_if = "Option::is_none")]
pub thickness: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
impl Default for ProgressElement {
fn default() -> Self {
Self {
value: 0.0,
total: 1.0,
label: None,
tint: None,
color: None,
bar_style: None,
start_at: None,
end_at: None,
track_color: None,
thickness: None,
style: ElementStyle::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GaugeElement {
pub value: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(
rename = "currentValueLabel",
default,
skip_serializing_if = "Option::is_none"
)]
pub current_value_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tint: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(
rename = "gaugeStyle",
default,
skip_serializing_if = "Option::is_none"
)]
pub gauge_style: Option<GaugeStyle>,
#[serde(rename = "startAt", default, skip_serializing_if = "Option::is_none")]
pub start_at: Option<u64>,
#[serde(rename = "endAt", default, skip_serializing_if = "Option::is_none")]
pub end_at: Option<u64>,
#[serde(rename = "minLabel", default, skip_serializing_if = "Option::is_none")]
pub min_label: Option<String>,
#[serde(rename = "maxLabel", default, skip_serializing_if = "Option::is_none")]
pub max_label: Option<String>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ButtonElement {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
)]
pub set: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(
rename = "backgroundColor",
default,
skip_serializing_if = "Option::is_none"
)]
pub background_color: Option<ColorValue>,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(
rename = "textAlignment",
default,
skip_serializing_if = "Option::is_none"
)]
pub text_alignment: Option<TextAlignment>,
#[serde(rename = "variant", default, skip_serializing_if = "Option::is_none")]
pub variant: Option<ButtonVariant>,
#[serde(
rename = "systemName",
default,
skip_serializing_if = "Option::is_none"
)]
pub system_name: Option<String>,
#[serde(rename = "disabled", default, skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ToggleElement {
#[serde(rename = "isOn")]
pub is_on: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tint: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
)]
pub set: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "variant", default, skip_serializing_if = "Option::is_none")]
pub variant: Option<ToggleVariant>,
#[serde(
rename = "uncheckedTint",
default,
skip_serializing_if = "Option::is_none"
)]
pub unchecked_tint: Option<ColorValue>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct DividerElement {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thickness: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct SpacerElement {
#[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
pub min_length: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct DateElement {
pub date: String,
#[serde(rename = "dateStyle", default, skip_serializing_if = "Option::is_none")]
pub date_style: Option<DateStyle>,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ChartElement {
#[serde(rename = "chartType")]
pub chart_type: ChartType,
#[serde(default, rename = "chartData", skip_serializing_if = "Vec::is_empty")]
pub chart_data: Vec<ChartDataPoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tint: Option<ColorValue>,
#[serde(rename = "showXAxis", default, skip_serializing_if = "Option::is_none")]
pub show_x_axis: Option<bool>,
#[serde(rename = "showYAxis", default, skip_serializing_if = "Option::is_none")]
pub show_y_axis: Option<bool>,
#[serde(
rename = "showLegend",
default,
skip_serializing_if = "Option::is_none"
)]
pub show_legend: Option<bool>,
#[serde(rename = "yMin", default, skip_serializing_if = "Option::is_none")]
pub y_min: Option<f64>,
#[serde(rename = "yMax", default, skip_serializing_if = "Option::is_none")]
pub y_max: Option<f64>,
#[serde(
rename = "interpolation",
default,
skip_serializing_if = "Option::is_none"
)]
pub interpolation: Option<ChartInterpolation>,
#[serde(
rename = "innerRadius",
default,
skip_serializing_if = "Option::is_none"
)]
pub inner_radius: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ListElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<ListItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct LinkElement {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<WidgetElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
)]
pub set: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ShapeElement {
#[serde(rename = "shapeType")]
pub shape_type: ShapeType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fill: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
pub stroke_width: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct TimerElement {
#[serde(rename = "targetDate")]
pub target_date: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub counting: Option<TimerCounting>,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(
rename = "fontWeight",
default,
skip_serializing_if = "Option::is_none"
)]
pub font_weight: Option<FontWeight>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(rename = "startDate", default, skip_serializing_if = "Option::is_none")]
pub start_date: Option<String>,
#[serde(rename = "showHours", default, skip_serializing_if = "Option::is_none")]
pub show_hours: Option<bool>,
#[serde(
rename = "completedText",
default,
skip_serializing_if = "Option::is_none"
)]
pub completed_text: Option<String>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct CanvasElement {
pub width: f64,
pub height: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub elements: Vec<CanvasDrawCommand>,
#[serde(flatten)]
pub style: ElementStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct LabelElement {
pub text: String,
#[serde(rename = "systemName")]
pub system_name: String,
#[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
pub icon_color: Option<ColorValue>,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<f64>,
#[serde(
rename = "fontWeight",
default,
skip_serializing_if = "Option::is_none"
)]
pub font_weight: Option<FontWeight>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(flatten)]
pub style: ElementStyle,
}
impl From<VStackElement> for WidgetElement {
fn from(value: VStackElement) -> Self {
WidgetElement::VStack(value)
}
}
impl From<HStackElement> for WidgetElement {
fn from(value: HStackElement) -> Self {
WidgetElement::HStack(value)
}
}
impl From<ZStackElement> for WidgetElement {
fn from(value: ZStackElement) -> Self {
WidgetElement::ZStack(value)
}
}
impl From<GridElement> for WidgetElement {
fn from(value: GridElement) -> Self {
WidgetElement::Grid(value)
}
}
impl From<ContainerElement> for WidgetElement {
fn from(value: ContainerElement) -> Self {
WidgetElement::Container(value)
}
}
impl From<TextElement> for WidgetElement {
fn from(value: TextElement) -> Self {
WidgetElement::Text(value)
}
}
impl From<ImageElement> for WidgetElement {
fn from(value: ImageElement) -> Self {
WidgetElement::Image(value)
}
}
impl From<ProgressElement> for WidgetElement {
fn from(value: ProgressElement) -> Self {
WidgetElement::Progress(value)
}
}
impl From<GaugeElement> for WidgetElement {
fn from(value: GaugeElement) -> Self {
WidgetElement::Gauge(value)
}
}
impl From<ButtonElement> for WidgetElement {
fn from(value: ButtonElement) -> Self {
WidgetElement::Button(value)
}
}
impl From<ToggleElement> for WidgetElement {
fn from(value: ToggleElement) -> Self {
WidgetElement::Toggle(value)
}
}
impl From<DividerElement> for WidgetElement {
fn from(value: DividerElement) -> Self {
WidgetElement::Divider(value)
}
}
impl From<SpacerElement> for WidgetElement {
fn from(value: SpacerElement) -> Self {
WidgetElement::Spacer(value)
}
}
impl From<DateElement> for WidgetElement {
fn from(value: DateElement) -> Self {
WidgetElement::Date(value)
}
}
impl From<ChartElement> for WidgetElement {
fn from(value: ChartElement) -> Self {
WidgetElement::Chart(value)
}
}
impl From<ListElement> for WidgetElement {
fn from(value: ListElement) -> Self {
WidgetElement::List(value)
}
}
impl From<LinkElement> for WidgetElement {
fn from(value: LinkElement) -> Self {
WidgetElement::Link(value)
}
}
impl From<ShapeElement> for WidgetElement {
fn from(value: ShapeElement) -> Self {
WidgetElement::Shape(value)
}
}
impl From<TimerElement> for WidgetElement {
fn from(value: TimerElement) -> Self {
WidgetElement::Timer(value)
}
}
impl From<CanvasElement> for WidgetElement {
fn from(value: CanvasElement) -> Self {
WidgetElement::Canvas(value)
}
}
impl From<LabelElement> for WidgetElement {
fn from(value: LabelElement) -> Self {
WidgetElement::Label(value)
}
}
pub fn text(content: impl Into<String>) -> TextElement {
TextElement {
content: content.into(),
..Default::default()
}
}
pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
VStackElement {
children,
..Default::default()
}
}
pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
HStackElement {
children,
..Default::default()
}
}
fn default_columns() -> u32 {
2
}
fn default_total() -> f64 {
1.0
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn build_serialize_parse_and_walk_layouts() {
let c = WidgetConfig::small(text("s"))
.with_medium(text("m"))
.with_large(hstack(vec![text("l").into()]));
assert!(c.small.is_some() && c.medium.is_some());
match c.large {
Some(WidgetElement::HStack(h)) => assert_eq!(h.children.len(), 1),
other => panic!("{other:?}"),
}
let cfg = WidgetConfig::small(vstack(vec![text("72°")
.font_size(36.0)
.font_weight(FontWeight::Bold)
.color("#fff")
.into()]));
let v = serde_json::to_value(&cfg).unwrap();
assert_eq!(v["small"]["type"], "vstack");
let child = &v["small"]["children"][0];
assert_eq!(child["type"], "text");
assert_eq!(child["content"], "72°");
assert_eq!(child["fontSize"], 36.0);
assert_eq!(child["fontWeight"], "bold");
assert_eq!(child["color"], "#fff");
let cfg = WidgetConfig::small(TextElement {
content: "hi".into(),
font_size: Some(12.0),
..Default::default()
});
let json = serde_json::to_string(&cfg).unwrap();
assert!(!json.contains("null"), "JSON must not contain null: {json}");
assert!(!json.contains("\"medium\""));
assert!(!json.contains("\"fontWeight\""));
let back: WidgetConfig = serde_json::from_str(&json).unwrap();
assert!(back.medium.is_none());
match back.small.unwrap() {
WidgetElement::Text(TextElement {
font_weight,
content,
..
}) => {
assert!(font_weight.is_none());
assert_eq!(content, "hi");
}
other => panic!("expected text, got {other:?}"),
}
let parsed: WidgetConfig = serde_json::from_str(r#"{"small":{"type":"grid"}}"#).unwrap();
assert_eq!(parsed.version, 1);
match parsed.small {
Some(WidgetElement::Grid(g)) => assert_eq!(g.columns, 2),
other => panic!("{other:?}"),
}
let w: WidgetWindowConfig =
serde_json::from_str(r#"{"label":"a","width":1,"height":1}"#).unwrap();
assert!(w.skip_taskbar);
assert!(!w.always_on_top);
let mut c: WidgetConfig = serde_json::from_value(serde_json::json!({
"small": {"type": "spacer"},
"entries": [{"at": 1, "medium": {"type": "spacer"}, "large": {"type": "spacer"}}]
}))
.unwrap();
assert_eq!(c.layouts_mut().count(), 3);
}
fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk_json_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("json") {
out.push(path);
}
}
}
#[test]
fn wire_format_unchanged() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let mut files = Vec::new();
walk_json_files(&root, &mut files);
assert!(
!files.is_empty(),
"expected fixtures under {}",
root.display()
);
for path in files {
let raw = std::fs::read_to_string(&path).unwrap();
let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
panic!("deserialize {}: {e}", path.display());
});
let encoded = serde_json::to_value(&cfg).unwrap();
let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
assert_eq!(
encoded,
serde_json::to_value(&again).unwrap(),
"wire drift in {}",
path.display()
);
let out = serde_json::to_string(&cfg).unwrap();
assert!(
!out.contains("null"),
"{} serialized with null: {out}",
path.display()
);
}
}
}