use serde::{Deserialize, Serialize};
#[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 size: Option<String>,
}
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>,
}
impl Default for WidgetConfig {
fn default() -> Self {
Self {
version: 1,
small: None,
medium: None,
large: None,
}
}
}
impl WidgetConfig {
pub fn small(el: impl Into<WidgetElement>) -> Self {
Self {
small: Some(el.into()),
..Default::default()
}
}
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),
}
#[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(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(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(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(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(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,
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(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")]
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(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(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(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 = "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(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(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
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ElementStyle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub padding: Option<PaddingValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background: Option<BackgroundValue>,
#[serde(
rename = "cornerRadius",
default,
skip_serializing_if = "Option::is_none"
)]
pub corner_radius: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opacity: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame: Option<FrameConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border: Option<BorderConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shadow: Option<ShadowConfig>,
#[serde(rename = "clipShape", default, skip_serializing_if = "Option::is_none")]
pub clip_shape: Option<ClipShape>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum ColorValue {
Solid(String),
Adaptive { light: String, dark: String },
}
impl From<&str> for ColorValue {
fn from(value: &str) -> Self {
ColorValue::Solid(value.to_string())
}
}
impl From<String> for ColorValue {
fn from(value: String) -> Self {
ColorValue::Solid(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ClipShape {
Circle,
Capsule,
Rectangle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TextStyle {
LargeTitle,
Title,
Title2,
Title3,
Headline,
Subheadline,
Body,
Callout,
Footnote,
Caption,
Caption2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum BackgroundValue {
Solid(String),
Gradient(GradientConfig),
Adaptive { light: String, dark: String },
}
impl From<&str> for BackgroundValue {
fn from(value: &str) -> Self {
BackgroundValue::Solid(value.to_string())
}
}
impl From<String> for BackgroundValue {
fn from(value: String) -> Self {
BackgroundValue::Solid(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GradientConfig {
#[serde(rename = "gradientType")]
pub gradient_type: GradientType,
pub colors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<GradientDirection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GradientType {
Linear,
Radial,
Angular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GradientDirection {
TopToBottom,
BottomToTop,
LeadingToTrailing,
TrailingToLeading,
TopLeadingToBottomTrailing,
TopTrailingToBottomLeading,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ShadowConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub radius: Option<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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum PaddingValue {
Uniform(f64),
Edges {
#[serde(default, skip_serializing_if = "Option::is_none")]
top: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bottom: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
leading: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
trailing: Option<f64>,
},
}
impl From<f64> for PaddingValue {
fn from(value: f64) -> Self {
PaddingValue::Uniform(value)
}
}
impl From<f32> for PaddingValue {
fn from(value: f32) -> Self {
PaddingValue::Uniform(f64::from(value))
}
}
impl From<i32> for PaddingValue {
fn from(value: i32) -> Self {
PaddingValue::Uniform(f64::from(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct FrameConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<f64>,
#[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
pub max_width: Option<FrameDimension>,
#[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
pub max_height: Option<FrameDimension>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum FrameDimension {
Fixed(f64),
Keyword(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct BorderConfig {
pub color: String,
#[serde(default = "default_border_width")]
pub width: f64,
}
fn default_border_width() -> f64 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum FontWeight {
Ultralight,
Thin,
Light,
Regular,
Medium,
Semibold,
Bold,
Heavy,
Black,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum FontDesign {
Default,
Monospaced,
Rounded,
Serif,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TextAlignment {
Leading,
Center,
Trailing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum HorizontalAlignment {
Leading,
Center,
Trailing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum VerticalAlignment {
Top,
Center,
Bottom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ContentMode {
Fit,
Fill,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ProgressStyle {
Linear,
Circular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GaugeStyle {
Circular,
Linear,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum DateStyle {
Time,
Date,
Relative,
Offset,
Timer,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ChartType {
#[default]
Bar,
Line,
Area,
Pie,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ShapeType {
#[default]
Circle,
Capsule,
Rectangle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TimerCounting {
Up,
Down,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ChartDataPoint {
pub label: String,
pub value: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ListItem {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checked: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "draw", rename_all = "camelCase")]
pub enum CanvasDrawCommand {
#[serde(rename = "circle")]
Circle {
cx: f64,
cy: f64,
r: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
fill: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
stroke_width: Option<f64>,
},
#[serde(rename = "line")]
Line {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
stroke_width: Option<f64>,
#[serde(rename = "lineCap", default, skip_serializing_if = "Option::is_none")]
line_cap: Option<String>,
},
#[serde(rename = "rect")]
Rect {
x: f64,
y: f64,
width: f64,
height: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
fill: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
stroke_width: Option<f64>,
#[serde(
rename = "cornerRadius",
default,
skip_serializing_if = "Option::is_none"
)]
corner_radius: Option<f64>,
},
#[serde(rename = "arc")]
Arc {
cx: f64,
cy: f64,
r: f64,
#[serde(rename = "startAngle")]
start_angle: f64,
#[serde(rename = "endAngle")]
end_angle: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
fill: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
stroke_width: Option<f64>,
},
#[serde(rename = "text")]
Text {
x: f64,
y: f64,
content: String,
#[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
font_size: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
color: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
anchor: Option<String>,
},
#[serde(rename = "path")]
Path {
d: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
fill: Option<ColorValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stroke: Option<ColorValue>,
#[serde(
rename = "strokeWidth",
default,
skip_serializing_if = "Option::is_none"
)]
stroke_width: Option<f64>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn option_none_omitted_from_json() {
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:?}"),
}
}
#[test]
fn ergonomic_builders_serialize() {
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");
assert_eq!(v["small"]["children"][0]["type"], "text");
assert_eq!(v["small"]["children"][0]["content"], "72°");
assert_eq!(v["small"]["children"][0]["fontSize"], 36.0);
assert_eq!(v["small"]["children"][0]["fontWeight"], "bold");
assert_eq!(v["small"]["children"][0]["color"], "#fff");
}
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()
);
}
}
}