1use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[cfg_attr(feature = "schema", derive(JsonSchema))]
12#[serde(rename_all = "camelCase")]
13pub struct ElementStyle {
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub padding: Option<PaddingValue>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub background: Option<BackgroundValue>,
20 #[serde(
22 rename = "cornerRadius",
23 default,
24 skip_serializing_if = "Option::is_none"
25 )]
26 pub corner_radius: Option<f64>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub opacity: Option<f64>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub frame: Option<FrameConfig>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub border: Option<BorderConfig>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub shadow: Option<ShadowConfig>,
39 #[serde(rename = "clipShape", default, skip_serializing_if = "Option::is_none")]
41 pub clip_shape: Option<ClipShape>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub flex: Option<f64>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(JsonSchema))]
53#[serde(untagged)]
54pub enum ColorValue {
55 Solid(String),
57 Adaptive {
59 light: String,
61 dark: String,
63 },
64}
65
66impl From<&str> for ColorValue {
67 fn from(value: &str) -> Self {
68 ColorValue::Solid(value.to_string())
69 }
70}
71
72impl From<String> for ColorValue {
73 fn from(value: String) -> Self {
74 ColorValue::Solid(value)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(JsonSchema))]
81#[serde(rename_all = "camelCase")]
82pub enum ClipShape {
83 Circle,
85 Capsule,
87 Rectangle,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[cfg_attr(feature = "schema", derive(JsonSchema))]
94#[serde(rename_all = "camelCase")]
95pub enum TextStyle {
96 LargeTitle,
98 Title,
100 Title2,
102 Title3,
104 Headline,
106 Subheadline,
108 Body,
110 Callout,
112 Footnote,
114 Caption,
116 Caption2,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[cfg_attr(feature = "schema", derive(JsonSchema))]
123#[serde(untagged)]
124pub enum BackgroundValue {
125 Solid(String),
127 Gradient(GradientConfig),
129 Adaptive {
131 light: String,
133 dark: String,
135 },
136}
137
138impl From<&str> for BackgroundValue {
139 fn from(value: &str) -> Self {
140 BackgroundValue::Solid(value.to_string())
141 }
142}
143
144impl From<String> for BackgroundValue {
145 fn from(value: String) -> Self {
146 BackgroundValue::Solid(value)
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152#[cfg_attr(feature = "schema", derive(JsonSchema))]
153#[serde(rename_all = "camelCase")]
154pub struct GradientConfig {
155 #[serde(rename = "gradientType")]
157 pub gradient_type: GradientType,
158 pub colors: Vec<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub direction: Option<GradientDirection>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "schema", derive(JsonSchema))]
168#[serde(rename_all = "camelCase")]
169pub enum GradientType {
170 Linear,
172 Radial,
174 Angular,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180#[cfg_attr(feature = "schema", derive(JsonSchema))]
181#[serde(rename_all = "camelCase")]
182pub enum GradientDirection {
183 TopToBottom,
185 BottomToTop,
187 LeadingToTrailing,
189 TrailingToLeading,
191 TopLeadingToBottomTrailing,
193 TopTrailingToBottomLeading,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[cfg_attr(feature = "schema", derive(JsonSchema))]
200#[serde(rename_all = "camelCase")]
201pub struct ShadowConfig {
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub color: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub radius: Option<f64>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub x: Option<f64>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub y: Option<f64>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218#[cfg_attr(feature = "schema", derive(JsonSchema))]
219#[serde(untagged)]
220pub enum PaddingValue {
221 Uniform(f64),
223 Edges {
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 top: Option<f64>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 bottom: Option<f64>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 leading: Option<f64>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 trailing: Option<f64>,
237 },
238}
239
240impl From<f64> for PaddingValue {
241 fn from(value: f64) -> Self {
242 PaddingValue::Uniform(value)
243 }
244}
245
246impl From<f32> for PaddingValue {
247 fn from(value: f32) -> Self {
248 PaddingValue::Uniform(f64::from(value))
249 }
250}
251
252impl From<i32> for PaddingValue {
253 fn from(value: i32) -> Self {
254 PaddingValue::Uniform(f64::from(value))
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260#[cfg_attr(feature = "schema", derive(JsonSchema))]
261#[serde(rename_all = "camelCase")]
262pub struct FrameConfig {
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub width: Option<f64>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub height: Option<f64>,
269 #[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
271 pub max_width: Option<FrameDimension>,
272 #[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
274 pub max_height: Option<FrameDimension>,
275}
276
277#[derive(Debug, Clone)]
279pub enum FrameDimension {
280 Fixed(f64),
282 Infinity,
284}
285
286#[cfg(feature = "schema")]
287impl JsonSchema for FrameDimension {
288 fn schema_name() -> String {
289 "FrameDimension".into()
290 }
291
292 fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
293 use schemars::schema::{
294 InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
295 };
296
297 Schema::Object(SchemaObject {
298 metadata: Some(Box::new(Metadata {
299 description: Some(
300 "A frame dimension — either a fixed point value or `\"infinity\"`.".into(),
301 ),
302 ..Default::default()
303 })),
304 subschemas: Some(Box::new(SubschemaValidation {
305 any_of: Some(vec![
306 SchemaObject {
307 metadata: Some(Box::new(Metadata {
308 description: Some("Fixed point value.".into()),
309 ..Default::default()
310 })),
311 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))),
312 format: Some("double".into()),
313 ..Default::default()
314 }
315 .into(),
316 SchemaObject {
317 metadata: Some(Box::new(Metadata {
318 description: Some("Keyword `\"infinity\"`.".into()),
319 ..Default::default()
320 })),
321 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
322 enum_values: Some(vec!["infinity".into()]),
323 ..Default::default()
324 }
325 .into(),
326 ]),
327 ..Default::default()
328 })),
329 ..Default::default()
330 })
331 }
332}
333
334impl Serialize for FrameDimension {
335 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
336 where
337 S: serde::Serializer,
338 {
339 match self {
340 Self::Fixed(v) => serializer.serialize_f64(*v),
341 Self::Infinity => serializer.serialize_str("infinity"),
342 }
343 }
344}
345
346impl<'de> Deserialize<'de> for FrameDimension {
347 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
348 where
349 D: serde::Deserializer<'de>,
350 {
351 #[derive(Deserialize)]
352 #[serde(untagged)]
353 enum Helper {
354 Num(f64),
355 Str(String),
356 }
357 match Helper::deserialize(deserializer)? {
358 Helper::Num(v) => Ok(Self::Fixed(v)),
359 Helper::Str(s) if s == "infinity" => Ok(Self::Infinity),
360 Helper::Str(s) => Err(serde::de::Error::custom(format!(
361 "unknown frame dimension keyword: {s}"
362 ))),
363 }
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369#[cfg_attr(feature = "schema", derive(JsonSchema))]
370#[serde(rename_all = "camelCase")]
371pub struct BorderConfig {
372 pub color: String,
374 #[serde(default = "default_border_width")]
376 pub width: f64,
377}
378
379fn default_border_width() -> f64 {
380 1.0
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385#[cfg_attr(feature = "schema", derive(JsonSchema))]
386#[serde(rename_all = "camelCase")]
387pub enum FontWeight {
388 Ultralight,
390 Thin,
392 Light,
394 Regular,
396 Medium,
398 Semibold,
400 Bold,
402 Heavy,
404 Black,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410#[cfg_attr(feature = "schema", derive(JsonSchema))]
411#[serde(rename_all = "camelCase")]
412pub enum FontDesign {
413 Default,
415 Monospaced,
417 Rounded,
419 Serif,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425#[cfg_attr(feature = "schema", derive(JsonSchema))]
426#[serde(rename_all = "camelCase")]
427pub enum TextAlignment {
428 Leading,
430 Center,
432 Trailing,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[cfg_attr(feature = "schema", derive(JsonSchema))]
439#[serde(rename_all = "camelCase")]
440pub enum HorizontalAlignment {
441 Leading,
443 Center,
445 Trailing,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451#[cfg_attr(feature = "schema", derive(JsonSchema))]
452#[serde(rename_all = "camelCase")]
453pub enum VerticalAlignment {
454 Top,
456 Center,
458 Bottom,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464#[cfg_attr(feature = "schema", derive(JsonSchema))]
465#[serde(rename_all = "camelCase")]
466pub enum ContentMode {
467 Fit,
469 Fill,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475#[cfg_attr(feature = "schema", derive(JsonSchema))]
476#[serde(rename_all = "camelCase")]
477pub enum ProgressStyle {
478 Linear,
480 Circular,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486#[cfg_attr(feature = "schema", derive(JsonSchema))]
487#[serde(rename_all = "camelCase")]
488pub enum GaugeStyle {
489 Circular,
491 Linear,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497#[cfg_attr(feature = "schema", derive(JsonSchema))]
498#[serde(rename_all = "camelCase")]
499pub enum DateStyle {
500 Time,
502 Date,
504 Relative,
506 Offset,
508 Timer,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514#[cfg_attr(feature = "schema", derive(JsonSchema))]
515#[serde(rename_all = "camelCase")]
516pub enum ChartType {
517 #[default]
519 Bar,
520 Line,
522 Area,
524 Pie,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, Default)]
530#[cfg_attr(feature = "schema", derive(JsonSchema))]
531#[serde(rename_all = "camelCase")]
532pub enum ShapeType {
533 #[default]
535 Circle,
536 Capsule,
538 Rectangle,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544#[cfg_attr(feature = "schema", derive(JsonSchema))]
545#[serde(rename_all = "camelCase")]
546pub enum TimerCounting {
547 Up,
549 Down,
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn color_value_roundtrip_solid_and_adaptive() {
559 let solid = ColorValue::Solid("#ff0000".into());
560 let s = serde_json::to_string(&solid).unwrap();
561 let back: ColorValue = serde_json::from_str(&s).unwrap();
562 assert!(matches!(back, ColorValue::Solid(ref x) if x == "#ff0000"));
563
564 let adaptive = ColorValue::Adaptive {
565 light: "#fff".into(),
566 dark: "#000".into(),
567 };
568 let s = serde_json::to_string(&adaptive).unwrap();
569 let back: ColorValue = serde_json::from_str(&s).unwrap();
570 assert!(matches!(
571 back,
572 ColorValue::Adaptive {
573 ref light,
574 ref dark
575 } if light == "#fff" && dark == "#000"
576 ));
577 }
578
579 #[test]
580 fn padding_value_roundtrip() {
581 let u = PaddingValue::Uniform(8.0);
582 let s = serde_json::to_string(&u).unwrap();
583 assert_eq!(s, "8.0");
584 let back: PaddingValue = serde_json::from_str(&s).unwrap();
585 assert!(matches!(back, PaddingValue::Uniform(v) if (v - 8.0).abs() < f64::EPSILON));
586
587 let edges = PaddingValue::Edges {
588 top: Some(1.0),
589 bottom: Some(2.0),
590 leading: Some(3.0),
591 trailing: None,
592 };
593 let s = serde_json::to_string(&edges).unwrap();
594 let back: PaddingValue = serde_json::from_str(&s).unwrap();
595 assert!(matches!(back, PaddingValue::Edges { .. }));
596 }
597
598 #[test]
599 fn element_style_default_is_empty_object() {
600 let s = serde_json::to_string(&ElementStyle::default()).unwrap();
601 assert_eq!(s, "{}");
602 }
603
604 #[test]
605 fn border_config_default_width() {
606 let b: BorderConfig =
607 serde_json::from_str(r##"{"color":"#ffffff"}"##).unwrap();
608 assert!((b.width - 1.0).abs() < f64::EPSILON);
609 assert_eq!(b.color, "#ffffff");
610 }
611
612 #[test]
613 fn chart_shape_defaults() {
614 assert!(matches!(ChartType::default(), ChartType::Bar));
615 assert!(matches!(ShapeType::default(), ShapeType::Circle));
616 }
617
618 #[test]
619 fn enums_deserialize_camel_case() {
620 let w: FontWeight = serde_json::from_str(r#""semibold""#).unwrap();
621 assert!(matches!(w, FontWeight::Semibold));
622 let a: TextAlignment = serde_json::from_str(r#""trailing""#).unwrap();
623 assert!(matches!(a, TextAlignment::Trailing));
624 let t: TimerCounting = serde_json::from_str(r#""down""#).unwrap();
625 assert!(matches!(t, TimerCounting::Down));
626 }
627
628 #[test]
629 fn frame_dimension_accepts_infinity() {
630 let d: FrameDimension = serde_json::from_str(r#""infinity""#).unwrap();
631 assert!(matches!(d, FrameDimension::Infinity));
632 }
633
634 #[test]
635 fn frame_dimension_rejects_unknown_keyword() {
636 assert!(serde_json::from_str::<FrameDimension>(r#""auto""#).is_err());
637 }
638}