use serde::{Deserialize, Serialize};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[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>,
#[serde(
rename = "privacySensitive",
default,
skip_serializing_if = "Option::is_none"
)]
pub privacy_sensitive: Option<bool>,
#[serde(
rename = "accessibilityLabel",
default,
skip_serializing_if = "Option::is_none"
)]
pub accessibility_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bind: Option<std::collections::BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin: Option<PaddingValue>,
#[serde(
rename = "aspectRatio",
default,
skip_serializing_if = "Option::is_none"
)]
pub aspect_ratio: Option<f64>,
#[serde(
rename = "layoutPriority",
default,
skip_serializing_if = "Option::is_none"
)]
pub layout_priority: Option<f64>,
#[serde(rename = "offsetX", default, skip_serializing_if = "Option::is_none")]
pub offset_x: Option<f64>,
#[serde(rename = "offsetY", default, skip_serializing_if = "Option::is_none")]
pub offset_y: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rotation: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scale: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accentable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
)]
pub when: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transition: Option<Transition>,
}
#[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 {
RoundedRectangle,
Ellipse,
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,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub colors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<GradientDirection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stops: Vec<GradientStop>,
#[serde(
rename = "startPoint",
default,
skip_serializing_if = "Option::is_none"
)]
pub start_point: Option<UnitPoint>,
#[serde(rename = "endPoint", default, skip_serializing_if = "Option::is_none")]
pub end_point: Option<UnitPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GradientStop {
pub color: String,
pub location: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct UnitPoint {
pub x: f64,
pub y: f64,
}
#[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>,
#[serde(rename = "minWidth", default, skip_serializing_if = "Option::is_none")]
pub min_width: Option<f64>,
#[serde(rename = "minHeight", default, skip_serializing_if = "Option::is_none")]
pub min_height: Option<f64>,
}
#[derive(Debug, Clone)]
pub enum FrameDimension {
Fixed(f64),
Infinity,
}
#[cfg(feature = "schema")]
impl JsonSchema for FrameDimension {
fn schema_name() -> String {
"FrameDimension".into()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{
InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
};
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(
"A frame dimension — either a fixed point value or `\"infinity\"`.".into(),
),
..Default::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some("Fixed point value.".into()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))),
format: Some("double".into()),
..Default::default()
}
.into(),
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some("Keyword `\"infinity\"`.".into()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(vec!["infinity".into()]),
..Default::default()
}
.into(),
]),
..Default::default()
})),
..Default::default()
})
}
}
impl Serialize for FrameDimension {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Fixed(v) => serializer.serialize_f64(*v),
Self::Infinity => serializer.serialize_str("infinity"),
}
}
}
impl<'de> Deserialize<'de> for FrameDimension {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Num(f64),
Str(String),
}
match Helper::deserialize(deserializer)? {
Helper::Num(v) => Ok(Self::Fixed(v)),
Helper::Str(s) if s == "infinity" => Ok(Self::Infinity),
Helper::Str(s) => Err(serde::de::Error::custom(format!(
"unknown frame dimension keyword: {s}"
))),
}
}
}
#[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,
Stretch,
Center,
}
#[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,
Point,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ShapeType {
#[default]
Circle,
Capsule,
Rectangle,
RoundedRectangle,
Ellipse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TimerCounting {
Up,
Down,
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn color_and_padding_roundtrip(
a in "[#a-zA-Z0-9]{0,8}",
b in "[#a-zA-Z0-9]{0,8}",
u in -4_000_000i32..4_000_000,
edges in proptest::array::uniform4(proptest::option::of(0u32..40_000)),
) {
let u = f64::from(u) / 4.0;
let edges = edges.map(|e| e.map(|q| f64::from(q) / 4.0));
for c in [ColorValue::Solid(a.clone()), ColorValue::Adaptive { light: a.clone(), dark: b.clone() }] {
let back: ColorValue = serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
match (&c, &back) {
(ColorValue::Solid(x), ColorValue::Solid(y)) => prop_assert_eq!(x, y),
(ColorValue::Adaptive { light: l1, dark: d1 }, ColorValue::Adaptive { light: l2, dark: d2 }) => {
prop_assert_eq!((l1, d1), (l2, d2))
}
_ => prop_assert!(false, "{:?} -> {:?}", c, back),
}
}
let back: PaddingValue = serde_json::from_str(&serde_json::to_string(&PaddingValue::Uniform(u)).unwrap()).unwrap();
prop_assert!(matches!(back, PaddingValue::Uniform(v) if v == u));
let [top, bottom, leading, trailing] = edges;
let p = PaddingValue::Edges { top, bottom, leading, trailing };
let back: PaddingValue = serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap();
match back {
PaddingValue::Edges { top: t, bottom: b, leading: l, trailing: tr } => {
prop_assert_eq!((t, b, l, tr), (top, bottom, leading, trailing))
}
other => prop_assert!(false, "{:?}", other),
}
}
}
#[test]
fn style_wire_spellings_and_defaults() {
assert_eq!(
serde_json::to_string(&PaddingValue::Uniform(8.0)).unwrap(),
"8.0"
);
assert_eq!(
serde_json::to_string(&ElementStyle::default()).unwrap(),
"{}"
);
let b: BorderConfig = serde_json::from_str(r##"{"color":"#ffffff"}"##).unwrap();
assert!((b.width - 1.0).abs() < f64::EPSILON);
assert_eq!(b.color, "#ffffff");
assert!(matches!(ChartType::default(), ChartType::Bar));
assert!(matches!(ShapeType::default(), ShapeType::Circle));
let w: FontWeight = serde_json::from_str(r#""semibold""#).unwrap();
assert!(matches!(w, FontWeight::Semibold));
let a: TextAlignment = serde_json::from_str(r#""trailing""#).unwrap();
assert!(matches!(a, TextAlignment::Trailing));
let t: TimerCounting = serde_json::from_str(r#""down""#).unwrap();
assert!(matches!(t, TimerCounting::Down));
let d: FrameDimension = serde_json::from_str(r#""infinity""#).unwrap();
assert!(matches!(d, FrameDimension::Infinity));
assert!(serde_json::from_str::<FrameDimension>(r#""auto""#).is_err());
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum Justify {
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TruncationMode {
Head,
Middle,
Tail,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum SymbolRendering {
Monochrome,
Hierarchical,
Palette,
Multicolor,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ButtonVariant {
Bordered,
Prominent,
Plain,
Outline,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ToggleVariant {
Switch,
Checkbox,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ChartInterpolation {
Linear,
Monotone,
Step,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum Transition {
None,
Opacity,
Slide,
Numeric,
}