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 #[serde(
48 rename = "privacySensitive",
49 default,
50 skip_serializing_if = "Option::is_none"
51 )]
52 pub privacy_sensitive: Option<bool>,
53 #[serde(
55 rename = "accessibilityLabel",
56 default,
57 skip_serializing_if = "Option::is_none"
58 )]
59 pub accessibility_label: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub bind: Option<std::collections::BTreeMap<String, String>>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub margin: Option<PaddingValue>,
67 #[serde(
69 rename = "aspectRatio",
70 default,
71 skip_serializing_if = "Option::is_none"
72 )]
73 pub aspect_ratio: Option<f64>,
74 #[serde(
76 rename = "layoutPriority",
77 default,
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub layout_priority: Option<f64>,
81 #[serde(rename = "offsetX", default, skip_serializing_if = "Option::is_none")]
83 pub offset_x: Option<f64>,
84 #[serde(rename = "offsetY", default, skip_serializing_if = "Option::is_none")]
86 pub offset_y: Option<f64>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub rotation: Option<f64>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub scale: Option<f64>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub accentable: Option<bool>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
100 #[cfg_attr(
101 feature = "schema",
102 schemars(with = "Option<serde_json::Map<String, serde_json::Value>>")
103 )]
104 pub when: Option<serde_json::Value>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub transition: Option<Transition>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
117#[cfg_attr(feature = "schema", derive(JsonSchema))]
118#[serde(untagged)]
119pub enum ColorValue {
120 Solid(String),
122 Adaptive {
124 light: String,
126 dark: String,
128 },
129}
130
131impl From<&str> for ColorValue {
132 fn from(value: &str) -> Self {
133 ColorValue::Solid(value.to_string())
134 }
135}
136
137impl From<String> for ColorValue {
138 fn from(value: String) -> Self {
139 ColorValue::Solid(value)
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145#[cfg_attr(feature = "schema", derive(JsonSchema))]
146#[serde(rename_all = "camelCase")]
147pub enum ClipShape {
148 RoundedRectangle,
150 Ellipse,
152 Circle,
154 Capsule,
156 Rectangle,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "schema", derive(JsonSchema))]
163#[serde(rename_all = "camelCase")]
164pub enum TextStyle {
165 LargeTitle,
167 Title,
169 Title2,
171 Title3,
173 Headline,
175 Subheadline,
177 Body,
179 Callout,
181 Footnote,
183 Caption,
185 Caption2,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191#[cfg_attr(feature = "schema", derive(JsonSchema))]
192#[serde(untagged)]
193pub enum BackgroundValue {
194 Solid(String),
196 Gradient(GradientConfig),
198 Adaptive {
200 light: String,
202 dark: String,
204 },
205}
206
207impl From<&str> for BackgroundValue {
208 fn from(value: &str) -> Self {
209 BackgroundValue::Solid(value.to_string())
210 }
211}
212
213impl From<String> for BackgroundValue {
214 fn from(value: String) -> Self {
215 BackgroundValue::Solid(value)
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "schema", derive(JsonSchema))]
222#[serde(rename_all = "camelCase")]
223pub struct GradientConfig {
224 #[serde(rename = "gradientType")]
226 pub gradient_type: GradientType,
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub colors: Vec<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub direction: Option<GradientDirection>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
235 pub stops: Vec<GradientStop>,
236 #[serde(
238 rename = "startPoint",
239 default,
240 skip_serializing_if = "Option::is_none"
241 )]
242 pub start_point: Option<UnitPoint>,
243 #[serde(rename = "endPoint", default, skip_serializing_if = "Option::is_none")]
245 pub end_point: Option<UnitPoint>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250#[cfg_attr(feature = "schema", derive(JsonSchema))]
251#[serde(rename_all = "camelCase")]
252pub struct GradientStop {
253 pub color: String,
255 pub location: f64,
257}
258
259#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
261#[cfg_attr(feature = "schema", derive(JsonSchema))]
262pub struct UnitPoint {
263 pub x: f64,
265 pub y: f64,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271#[cfg_attr(feature = "schema", derive(JsonSchema))]
272#[serde(rename_all = "camelCase")]
273pub enum GradientType {
274 Linear,
276 Radial,
278 Angular,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(JsonSchema))]
285#[serde(rename_all = "camelCase")]
286pub enum GradientDirection {
287 TopToBottom,
289 BottomToTop,
291 LeadingToTrailing,
293 TrailingToLeading,
295 TopLeadingToBottomTrailing,
297 TopTrailingToBottomLeading,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303#[cfg_attr(feature = "schema", derive(JsonSchema))]
304#[serde(rename_all = "camelCase")]
305pub struct ShadowConfig {
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub color: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub radius: Option<f64>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub x: Option<f64>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub y: Option<f64>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322#[cfg_attr(feature = "schema", derive(JsonSchema))]
323#[serde(untagged)]
324pub enum PaddingValue {
325 Uniform(f64),
327 Edges {
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 top: Option<f64>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 bottom: Option<f64>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 leading: Option<f64>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 trailing: Option<f64>,
341 },
342}
343
344impl From<f64> for PaddingValue {
345 fn from(value: f64) -> Self {
346 PaddingValue::Uniform(value)
347 }
348}
349
350impl From<f32> for PaddingValue {
351 fn from(value: f32) -> Self {
352 PaddingValue::Uniform(f64::from(value))
353 }
354}
355
356impl From<i32> for PaddingValue {
357 fn from(value: i32) -> Self {
358 PaddingValue::Uniform(f64::from(value))
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364#[cfg_attr(feature = "schema", derive(JsonSchema))]
365#[serde(rename_all = "camelCase")]
366pub struct FrameConfig {
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub width: Option<f64>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub height: Option<f64>,
373 #[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
375 pub max_width: Option<FrameDimension>,
376 #[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
378 pub max_height: Option<FrameDimension>,
379 #[serde(rename = "minWidth", default, skip_serializing_if = "Option::is_none")]
381 pub min_width: Option<f64>,
382 #[serde(rename = "minHeight", default, skip_serializing_if = "Option::is_none")]
384 pub min_height: Option<f64>,
385}
386
387#[derive(Debug, Clone)]
389pub enum FrameDimension {
390 Fixed(f64),
392 Infinity,
394}
395
396#[cfg(feature = "schema")]
397impl JsonSchema for FrameDimension {
398 fn schema_name() -> String {
399 "FrameDimension".into()
400 }
401
402 fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
403 use schemars::schema::{
404 InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
405 };
406
407 Schema::Object(SchemaObject {
408 metadata: Some(Box::new(Metadata {
409 description: Some(
410 "A frame dimension — either a fixed point value or `\"infinity\"`.".into(),
411 ),
412 ..Default::default()
413 })),
414 subschemas: Some(Box::new(SubschemaValidation {
415 any_of: Some(vec![
416 SchemaObject {
417 metadata: Some(Box::new(Metadata {
418 description: Some("Fixed point value.".into()),
419 ..Default::default()
420 })),
421 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))),
422 format: Some("double".into()),
423 ..Default::default()
424 }
425 .into(),
426 SchemaObject {
427 metadata: Some(Box::new(Metadata {
428 description: Some("Keyword `\"infinity\"`.".into()),
429 ..Default::default()
430 })),
431 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
432 enum_values: Some(vec!["infinity".into()]),
433 ..Default::default()
434 }
435 .into(),
436 ]),
437 ..Default::default()
438 })),
439 ..Default::default()
440 })
441 }
442}
443
444impl Serialize for FrameDimension {
445 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
446 where
447 S: serde::Serializer,
448 {
449 match self {
450 Self::Fixed(v) => serializer.serialize_f64(*v),
451 Self::Infinity => serializer.serialize_str("infinity"),
452 }
453 }
454}
455
456impl<'de> Deserialize<'de> for FrameDimension {
457 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458 where
459 D: serde::Deserializer<'de>,
460 {
461 #[derive(Deserialize)]
462 #[serde(untagged)]
463 enum Helper {
464 Num(f64),
465 Str(String),
466 }
467 match Helper::deserialize(deserializer)? {
468 Helper::Num(v) => Ok(Self::Fixed(v)),
469 Helper::Str(s) if s == "infinity" => Ok(Self::Infinity),
470 Helper::Str(s) => Err(serde::de::Error::custom(format!(
471 "unknown frame dimension keyword: {s}"
472 ))),
473 }
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(JsonSchema))]
480#[serde(rename_all = "camelCase")]
481pub struct BorderConfig {
482 pub color: String,
484 #[serde(default = "default_border_width")]
486 pub width: f64,
487}
488
489fn default_border_width() -> f64 {
490 1.0
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495#[cfg_attr(feature = "schema", derive(JsonSchema))]
496#[serde(rename_all = "camelCase")]
497pub enum FontWeight {
498 Ultralight,
500 Thin,
502 Light,
504 Regular,
506 Medium,
508 Semibold,
510 Bold,
512 Heavy,
514 Black,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg_attr(feature = "schema", derive(JsonSchema))]
521#[serde(rename_all = "camelCase")]
522pub enum FontDesign {
523 Default,
525 Monospaced,
527 Rounded,
529 Serif,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535#[cfg_attr(feature = "schema", derive(JsonSchema))]
536#[serde(rename_all = "camelCase")]
537pub enum TextAlignment {
538 Leading,
540 Center,
542 Trailing,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
548#[cfg_attr(feature = "schema", derive(JsonSchema))]
549#[serde(rename_all = "camelCase")]
550pub enum HorizontalAlignment {
551 Leading,
553 Center,
555 Trailing,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
561#[cfg_attr(feature = "schema", derive(JsonSchema))]
562#[serde(rename_all = "camelCase")]
563pub enum VerticalAlignment {
564 Top,
566 Center,
568 Bottom,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574#[cfg_attr(feature = "schema", derive(JsonSchema))]
575#[serde(rename_all = "camelCase")]
576pub enum ContentMode {
577 Fit,
579 Fill,
581 Stretch,
583 Center,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
589#[cfg_attr(feature = "schema", derive(JsonSchema))]
590#[serde(rename_all = "camelCase")]
591pub enum ProgressStyle {
592 Linear,
594 Circular,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
600#[cfg_attr(feature = "schema", derive(JsonSchema))]
601#[serde(rename_all = "camelCase")]
602pub enum GaugeStyle {
603 Circular,
605 Linear,
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
611#[cfg_attr(feature = "schema", derive(JsonSchema))]
612#[serde(rename_all = "camelCase")]
613pub enum DateStyle {
614 Time,
616 Date,
618 Relative,
620 Offset,
622 Timer,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, Default)]
628#[cfg_attr(feature = "schema", derive(JsonSchema))]
629#[serde(rename_all = "camelCase")]
630pub enum ChartType {
631 #[default]
633 Bar,
634 Line,
636 Area,
638 Pie,
640 Point,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize, Default)]
646#[cfg_attr(feature = "schema", derive(JsonSchema))]
647#[serde(rename_all = "camelCase")]
648pub enum ShapeType {
649 #[default]
651 Circle,
652 Capsule,
654 Rectangle,
656 RoundedRectangle,
658 Ellipse,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
664#[cfg_attr(feature = "schema", derive(JsonSchema))]
665#[serde(rename_all = "camelCase")]
666pub enum TimerCounting {
667 Up,
669 Down,
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use proptest::prelude::*;
677
678 proptest! {
679 #[test]
682 fn color_and_padding_roundtrip(
683 a in "[#a-zA-Z0-9]{0,8}",
684 b in "[#a-zA-Z0-9]{0,8}",
685 u in -4_000_000i32..4_000_000,
686 edges in proptest::array::uniform4(proptest::option::of(0u32..40_000)),
687 ) {
688 let u = f64::from(u) / 4.0;
691 let edges = edges.map(|e| e.map(|q| f64::from(q) / 4.0));
692 for c in [ColorValue::Solid(a.clone()), ColorValue::Adaptive { light: a.clone(), dark: b.clone() }] {
693 let back: ColorValue = serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
694 match (&c, &back) {
695 (ColorValue::Solid(x), ColorValue::Solid(y)) => prop_assert_eq!(x, y),
696 (ColorValue::Adaptive { light: l1, dark: d1 }, ColorValue::Adaptive { light: l2, dark: d2 }) => {
697 prop_assert_eq!((l1, d1), (l2, d2))
698 }
699 _ => prop_assert!(false, "{:?} -> {:?}", c, back),
700 }
701 }
702 let back: PaddingValue = serde_json::from_str(&serde_json::to_string(&PaddingValue::Uniform(u)).unwrap()).unwrap();
703 prop_assert!(matches!(back, PaddingValue::Uniform(v) if v == u));
704 let [top, bottom, leading, trailing] = edges;
705 let p = PaddingValue::Edges { top, bottom, leading, trailing };
706 let back: PaddingValue = serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap();
707 match back {
708 PaddingValue::Edges { top: t, bottom: b, leading: l, trailing: tr } => {
709 prop_assert_eq!((t, b, l, tr), (top, bottom, leading, trailing))
710 }
711 other => prop_assert!(false, "{:?}", other),
713 }
714 }
715 }
716
717 #[test]
721 fn style_wire_spellings_and_defaults() {
722 assert_eq!(
723 serde_json::to_string(&PaddingValue::Uniform(8.0)).unwrap(),
724 "8.0"
725 );
726 assert_eq!(
727 serde_json::to_string(&ElementStyle::default()).unwrap(),
728 "{}"
729 );
730 let b: BorderConfig = serde_json::from_str(r##"{"color":"#ffffff"}"##).unwrap();
731 assert!((b.width - 1.0).abs() < f64::EPSILON);
732 assert_eq!(b.color, "#ffffff");
733 assert!(matches!(ChartType::default(), ChartType::Bar));
734 assert!(matches!(ShapeType::default(), ShapeType::Circle));
735 let w: FontWeight = serde_json::from_str(r#""semibold""#).unwrap();
736 assert!(matches!(w, FontWeight::Semibold));
737 let a: TextAlignment = serde_json::from_str(r#""trailing""#).unwrap();
738 assert!(matches!(a, TextAlignment::Trailing));
739 let t: TimerCounting = serde_json::from_str(r#""down""#).unwrap();
740 assert!(matches!(t, TimerCounting::Down));
741 let d: FrameDimension = serde_json::from_str(r#""infinity""#).unwrap();
742 assert!(matches!(d, FrameDimension::Infinity));
743 assert!(serde_json::from_str::<FrameDimension>(r#""auto""#).is_err());
744 }
745}
746
747#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
749#[cfg_attr(feature = "schema", derive(JsonSchema))]
750#[serde(rename_all = "camelCase")]
751pub enum Justify {
752 Start,
754 Center,
756 End,
758 SpaceBetween,
760 SpaceAround,
762 SpaceEvenly,
764}
765
766#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
768#[cfg_attr(feature = "schema", derive(JsonSchema))]
769#[serde(rename_all = "camelCase")]
770pub enum TruncationMode {
771 Head,
773 Middle,
775 Tail,
777}
778
779#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
781#[cfg_attr(feature = "schema", derive(JsonSchema))]
782#[serde(rename_all = "camelCase")]
783pub enum SymbolRendering {
784 Monochrome,
786 Hierarchical,
788 Palette,
790 Multicolor,
792}
793
794#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
796#[cfg_attr(feature = "schema", derive(JsonSchema))]
797#[serde(rename_all = "camelCase")]
798pub enum ButtonVariant {
799 Bordered,
801 Prominent,
803 Plain,
805 Outline,
807}
808
809#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
811#[cfg_attr(feature = "schema", derive(JsonSchema))]
812#[serde(rename_all = "camelCase")]
813pub enum ToggleVariant {
814 Switch,
816 Checkbox,
818}
819
820#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
822#[cfg_attr(feature = "schema", derive(JsonSchema))]
823#[serde(rename_all = "camelCase")]
824pub enum ChartInterpolation {
825 Linear,
827 Monotone,
829 Step,
831}
832
833#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
835#[cfg_attr(feature = "schema", derive(JsonSchema))]
836#[serde(rename_all = "camelCase")]
837pub enum Transition {
838 None,
840 Opacity,
842 Slide,
844 Numeric,
846}