Skip to main content

tauri_plugin_widgets/models/
style.rs

1//! Shared style values: colors, backgrounds, gradients, shadows, padding,
2//! frame/border config, and the small enums used across element fields.
3
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8
9/// Shared visual style applied to any element (padding, background, frame, …).
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[cfg_attr(feature = "schema", derive(JsonSchema))]
12#[serde(rename_all = "camelCase")]
13pub struct ElementStyle {
14    /// Inset padding (number or per-edge object).
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub padding: Option<PaddingValue>,
17    /// Solid, adaptive, or gradient background.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub background: Option<BackgroundValue>,
20    /// Corner radius in points.
21    #[serde(
22        rename = "cornerRadius",
23        default,
24        skip_serializing_if = "Option::is_none"
25    )]
26    pub corner_radius: Option<f64>,
27    /// Opacity from `0` (invisible) to `1` (opaque).
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub opacity: Option<f64>,
30    /// Explicit width / height / max constraints.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub frame: Option<FrameConfig>,
33    /// Border color and width.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub border: Option<BorderConfig>,
36    /// Drop shadow.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub shadow: Option<ShadowConfig>,
39    /// Clip content to a shape (e.g. circle avatar from square image).
40    #[serde(rename = "clipShape", default, skip_serializing_if = "Option::is_none")]
41    pub clip_shape: Option<ClipShape>,
42    /// Layout weight for flexible sizing inside stacks (like Android `layout_weight`).
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub flex: Option<f64>,
45    /// Hide this content where the OS redacts private data (iOS / macOS locked screen,
46    /// StandBy). Ignored where no redaction exists.
47    #[serde(
48        rename = "privacySensitive",
49        default,
50        skip_serializing_if = "Option::is_none"
51    )]
52    pub privacy_sensitive: Option<bool>,
53    /// Text read by screen readers instead of the visible content.
54    #[serde(
55        rename = "accessibilityLabel",
56        default,
57        skip_serializing_if = "Option::is_none"
58    )]
59    pub accessibility_label: Option<String>,
60    /// Set fields from data with their JSON type: `{"value": "data.progress"}`.
61    /// Use for numbers / booleans / arrays (`{{…}}` only fills strings).
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub bind: Option<std::collections::BTreeMap<String, String>>,
64    /// Outer spacing (number or per-edge object), outside background and border.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub margin: Option<PaddingValue>,
67    /// Keep width / height at this ratio (`1` = square).
68    #[serde(
69        rename = "aspectRatio",
70        default,
71        skip_serializing_if = "Option::is_none"
72    )]
73    pub aspect_ratio: Option<f64>,
74    /// Who keeps its size in a crowded stack: higher first. Default `0`.
75    #[serde(
76        rename = "layoutPriority",
77        default,
78        skip_serializing_if = "Option::is_none"
79    )]
80    pub layout_priority: Option<f64>,
81    /// Move right by points after layout (does not affect siblings).
82    #[serde(rename = "offsetX", default, skip_serializing_if = "Option::is_none")]
83    pub offset_x: Option<f64>,
84    /// Move down by points after layout (does not affect siblings).
85    #[serde(rename = "offsetY", default, skip_serializing_if = "Option::is_none")]
86    pub offset_y: Option<f64>,
87    /// Rotate by degrees (clockwise).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub rotation: Option<f64>,
90    /// Scale factor (`1` = unchanged).
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub scale: Option<f64>,
93    /// Apple tinted / accented Home Screen: draw this part in the accent color
94    /// (`widgetAccentable`). Ignored elsewhere.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub accentable: Option<bool>,
97    /// Show only when the condition holds (`{"path": "data.n", "gt": 0}`; see
98    /// [`crate::resolve`]). Removed before rendering.
99    #[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    /// How this element changes between timeline entries / redraws (a hint):
106    /// `numeric` animates digits, `opacity` cross-fades, `slide` pushes from the bottom.
107    /// Apple and desktop animate; Android and Adaptive Cards ignore it.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub transition: Option<Transition>,
110}
111
112/// Color value — hex string, semantic name, or adaptive `{ light, dark }` pair.
113///
114/// Semantic names: `"label"`, `"secondaryLabel"`, `"systemBackground"`,
115/// `"secondarySystemBackground"`, `"accent"`, `"separator"`.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[cfg_attr(feature = "schema", derive(JsonSchema))]
118#[serde(untagged)]
119pub enum ColorValue {
120    /// Hex string or semantic color name.
121    Solid(String),
122    /// Distinct colors for light and dark appearance.
123    Adaptive {
124        /// Color used in light appearance.
125        light: String,
126        /// Color used in dark appearance.
127        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/// Clip shape for content masking.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[cfg_attr(feature = "schema", derive(JsonSchema))]
146#[serde(rename_all = "camelCase")]
147pub enum ClipShape {
148    /// `roundedRectangle` (uses `cornerRadius`).
149    RoundedRectangle,
150    /// `ellipse`.
151    Ellipse,
152    /// `circle`.
153    Circle,
154    /// `capsule`.
155    Capsule,
156    /// `rectangle`.
157    Rectangle,
158}
159
160/// Semantic text style — respects Dynamic Type / accessibility settings.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "schema", derive(JsonSchema))]
163#[serde(rename_all = "camelCase")]
164pub enum TextStyle {
165    /// `large title`.
166    LargeTitle,
167    /// `title`.
168    Title,
169    /// `title2`.
170    Title2,
171    /// `title3`.
172    Title3,
173    /// `headline`.
174    Headline,
175    /// `subheadline`.
176    Subheadline,
177    /// `body`.
178    Body,
179    /// `callout`.
180    Callout,
181    /// `footnote`.
182    Footnote,
183    /// `caption`.
184    Caption,
185    /// `caption2`.
186    Caption2,
187}
188
189/// Background: solid color string, adaptive pair, gradient, or material blur.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[cfg_attr(feature = "schema", derive(JsonSchema))]
192#[serde(untagged)]
193pub enum BackgroundValue {
194    /// Hex string or semantic color name.
195    Solid(String),
196    /// Linear, radial, or angular gradient.
197    Gradient(GradientConfig),
198    /// Distinct colors for light and dark appearance.
199    Adaptive {
200        /// Color used in light appearance.
201        light: String,
202        /// Color used in dark appearance.
203        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/// Linear, radial, or angular gradient background.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "schema", derive(JsonSchema))]
222#[serde(rename_all = "camelCase")]
223pub struct GradientConfig {
224    /// `"linear"`, `"radial"`, or `"angular"`
225    #[serde(rename = "gradientType")]
226    pub gradient_type: GradientType,
227    /// Stop colors, in order.
228    #[serde(default, skip_serializing_if = "Vec::is_empty")]
229    pub colors: Vec<String>,
230    /// Direction for linear gradients
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub direction: Option<GradientDirection>,
233    /// Colors at explicit positions (`location` 0…1); replaces `colors` when set.
234    #[serde(default, skip_serializing_if = "Vec::is_empty")]
235    pub stops: Vec<GradientStop>,
236    /// Start point in unit space (`{x:0,y:0}` = top leading); overrides `direction`.
237    #[serde(
238        rename = "startPoint",
239        default,
240        skip_serializing_if = "Option::is_none"
241    )]
242    pub start_point: Option<UnitPoint>,
243    /// End point in unit space (`{x:1,y:1}` = bottom trailing).
244    #[serde(rename = "endPoint", default, skip_serializing_if = "Option::is_none")]
245    pub end_point: Option<UnitPoint>,
246}
247
248/// A gradient color at a position.
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250#[cfg_attr(feature = "schema", derive(JsonSchema))]
251#[serde(rename_all = "camelCase")]
252pub struct GradientStop {
253    /// Hex color.
254    pub color: String,
255    /// Position `0…1` along the gradient.
256    pub location: f64,
257}
258
259/// A point in unit coordinates (`0…1` on both axes, origin top leading).
260#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
261#[cfg_attr(feature = "schema", derive(JsonSchema))]
262pub struct UnitPoint {
263    /// Horizontal position, `0` = leading.
264    pub x: f64,
265    /// Vertical position, `0` = top.
266    pub y: f64,
267}
268
269/// Gradient shape.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[cfg_attr(feature = "schema", derive(JsonSchema))]
272#[serde(rename_all = "camelCase")]
273pub enum GradientType {
274    /// `linear`.
275    Linear,
276    /// `radial`.
277    Radial,
278    /// `angular`.
279    Angular,
280}
281
282/// Direction for linear gradients.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(JsonSchema))]
285#[serde(rename_all = "camelCase")]
286pub enum GradientDirection {
287    /// `top to bottom`.
288    TopToBottom,
289    /// `bottom to top`.
290    BottomToTop,
291    /// `leading to trailing`.
292    LeadingToTrailing,
293    /// `trailing to leading`.
294    TrailingToLeading,
295    /// `top leading to bottom trailing`.
296    TopLeadingToBottomTrailing,
297    /// `top trailing to bottom leading`.
298    TopTrailingToBottomLeading,
299}
300
301/// Drop shadow configuration.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[cfg_attr(feature = "schema", derive(JsonSchema))]
304#[serde(rename_all = "camelCase")]
305pub struct ShadowConfig {
306    /// Shadow color (hex string or semantic name).
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub color: Option<String>,
309    /// Blur radius in points.
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub radius: Option<f64>,
312    /// Horizontal offset in points.
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub x: Option<f64>,
315    /// Vertical offset in points.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub y: Option<f64>,
318}
319
320/// Inset padding — a single uniform value or per-edge overrides.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322#[cfg_attr(feature = "schema", derive(JsonSchema))]
323#[serde(untagged)]
324pub enum PaddingValue {
325    /// Same padding on every edge.
326    Uniform(f64),
327    /// Independent padding per edge.
328    Edges {
329        /// Top edge padding.
330        #[serde(default, skip_serializing_if = "Option::is_none")]
331        top: Option<f64>,
332        /// Bottom edge padding.
333        #[serde(default, skip_serializing_if = "Option::is_none")]
334        bottom: Option<f64>,
335        /// Leading (left in LTR) edge padding.
336        #[serde(default, skip_serializing_if = "Option::is_none")]
337        leading: Option<f64>,
338        /// Trailing (right in LTR) edge padding.
339        #[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/// Explicit width / height / max-size constraints for an element.
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[cfg_attr(feature = "schema", derive(JsonSchema))]
365#[serde(rename_all = "camelCase")]
366pub struct FrameConfig {
367    /// Fixed width in points.
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub width: Option<f64>,
370    /// Fixed height in points.
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub height: Option<f64>,
373    /// Maximum width — a fixed point value or `"infinity"`.
374    #[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
375    pub max_width: Option<FrameDimension>,
376    /// Maximum height — a fixed point value or `"infinity"`.
377    #[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
378    pub max_height: Option<FrameDimension>,
379    /// Minimum width in points.
380    #[serde(rename = "minWidth", default, skip_serializing_if = "Option::is_none")]
381    pub min_width: Option<f64>,
382    /// Minimum height in points.
383    #[serde(rename = "minHeight", default, skip_serializing_if = "Option::is_none")]
384    pub min_height: Option<f64>,
385}
386
387/// A frame dimension — either a fixed point value or `"infinity"`.
388#[derive(Debug, Clone)]
389pub enum FrameDimension {
390    /// Fixed point value.
391    Fixed(f64),
392    /// `"infinity"`.
393    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/// Border color and width.
478#[derive(Debug, Clone, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(JsonSchema))]
480#[serde(rename_all = "camelCase")]
481pub struct BorderConfig {
482    /// Border color (hex string or semantic name).
483    pub color: String,
484    /// Border width in points. Default `1.0`.
485    #[serde(default = "default_border_width")]
486    pub width: f64,
487}
488
489fn default_border_width() -> f64 {
490    1.0
491}
492
493/// Font weight.
494#[derive(Debug, Clone, Serialize, Deserialize)]
495#[cfg_attr(feature = "schema", derive(JsonSchema))]
496#[serde(rename_all = "camelCase")]
497pub enum FontWeight {
498    /// `ultralight`.
499    Ultralight,
500    /// `thin`.
501    Thin,
502    /// `light`.
503    Light,
504    /// `regular`.
505    Regular,
506    /// `medium`.
507    Medium,
508    /// `semibold`.
509    Semibold,
510    /// `bold`.
511    Bold,
512    /// `heavy`.
513    Heavy,
514    /// `black`.
515    Black,
516}
517
518/// Font design (default, monospaced, rounded, serif).
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg_attr(feature = "schema", derive(JsonSchema))]
521#[serde(rename_all = "camelCase")]
522pub enum FontDesign {
523    /// `default`.
524    Default,
525    /// `monospaced`.
526    Monospaced,
527    /// `rounded`.
528    Rounded,
529    /// `serif`.
530    Serif,
531}
532
533/// Text alignment within the line.
534#[derive(Debug, Clone, Serialize, Deserialize)]
535#[cfg_attr(feature = "schema", derive(JsonSchema))]
536#[serde(rename_all = "camelCase")]
537pub enum TextAlignment {
538    /// `leading`.
539    Leading,
540    /// `center`.
541    Center,
542    /// `trailing`.
543    Trailing,
544}
545
546/// Horizontal alignment of children within a stack.
547#[derive(Debug, Clone, Serialize, Deserialize)]
548#[cfg_attr(feature = "schema", derive(JsonSchema))]
549#[serde(rename_all = "camelCase")]
550pub enum HorizontalAlignment {
551    /// `leading`.
552    Leading,
553    /// `center`.
554    Center,
555    /// `trailing`.
556    Trailing,
557}
558
559/// Vertical alignment of children within a stack.
560#[derive(Debug, Clone, Serialize, Deserialize)]
561#[cfg_attr(feature = "schema", derive(JsonSchema))]
562#[serde(rename_all = "camelCase")]
563pub enum VerticalAlignment {
564    /// `top`.
565    Top,
566    /// `center`.
567    Center,
568    /// `bottom`.
569    Bottom,
570}
571
572/// How an image fills its frame.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574#[cfg_attr(feature = "schema", derive(JsonSchema))]
575#[serde(rename_all = "camelCase")]
576pub enum ContentMode {
577    /// `fit`.
578    Fit,
579    /// `fill`.
580    Fill,
581    /// `stretch`: fill both axes, ignoring the aspect ratio.
582    Stretch,
583    /// `center`: natural size, centered, no scaling.
584    Center,
585}
586
587/// Progress indicator style.
588#[derive(Debug, Clone, Serialize, Deserialize)]
589#[cfg_attr(feature = "schema", derive(JsonSchema))]
590#[serde(rename_all = "camelCase")]
591pub enum ProgressStyle {
592    /// `linear`.
593    Linear,
594    /// `circular`.
595    Circular,
596}
597
598/// Gauge visual style.
599#[derive(Debug, Clone, Serialize, Deserialize)]
600#[cfg_attr(feature = "schema", derive(JsonSchema))]
601#[serde(rename_all = "camelCase")]
602pub enum GaugeStyle {
603    /// `circular`.
604    Circular,
605    /// `linear`.
606    Linear,
607}
608
609/// Date / relative-time display style.
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[cfg_attr(feature = "schema", derive(JsonSchema))]
612#[serde(rename_all = "camelCase")]
613pub enum DateStyle {
614    /// `time`.
615    Time,
616    /// `date`.
617    Date,
618    /// `relative`.
619    Relative,
620    /// `offset`.
621    Offset,
622    /// `timer`.
623    Timer,
624}
625
626/// Chart kind.
627#[derive(Debug, Clone, Serialize, Deserialize, Default)]
628#[cfg_attr(feature = "schema", derive(JsonSchema))]
629#[serde(rename_all = "camelCase")]
630pub enum ChartType {
631    /// `bar`.
632    #[default]
633    Bar,
634    /// `line`.
635    Line,
636    /// `area`.
637    Area,
638    /// `pie` (donut with `innerRadius`).
639    Pie,
640    /// `point`: scatter dots.
641    Point,
642}
643
644/// Shape kind for [`crate::models::ShapeElement`].
645#[derive(Debug, Clone, Serialize, Deserialize, Default)]
646#[cfg_attr(feature = "schema", derive(JsonSchema))]
647#[serde(rename_all = "camelCase")]
648pub enum ShapeType {
649    /// `circle`.
650    #[default]
651    Circle,
652    /// `capsule`.
653    Capsule,
654    /// `rectangle`.
655    Rectangle,
656    /// `roundedRectangle` (uses `cornerRadius`).
657    RoundedRectangle,
658    /// `ellipse`.
659    Ellipse,
660}
661
662/// Countdown/countup direction for [`crate::models::TimerElement`].
663#[derive(Debug, Clone, Serialize, Deserialize)]
664#[cfg_attr(feature = "schema", derive(JsonSchema))]
665#[serde(rename_all = "camelCase")]
666pub enum TimerCounting {
667    /// `up`.
668    Up,
669    /// `down`.
670    Down,
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use proptest::prelude::*;
677
678    proptest! {
679        /// Any solid / adaptive colour and any uniform / per-edge padding survives
680        /// serialize → deserialize with the same variant and values.
681        #[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            // Quarter steps: exact in f64 and in JSON (serde_json's default float parser
689            // is not round-trip exact for arbitrary f64).
690            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                // All-`None` edges serialize to `{}`, which is still an edges object.
712                other => prop_assert!(false, "{:?}", other),
713            }
714        }
715    }
716
717    /// Wire spellings and defaults of the style vocabulary: uniform padding is a bare
718    /// number → an empty style is `{}` → border width defaults to 1 → chart / shape
719    /// defaults → camelCase enums → `infinity` is the only frame keyword.
720    #[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/// Distribution of stack children along the stack axis.
748#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
749#[cfg_attr(feature = "schema", derive(JsonSchema))]
750#[serde(rename_all = "camelCase")]
751pub enum Justify {
752    /// Packed at the start (default).
753    Start,
754    /// Packed in the middle.
755    Center,
756    /// Packed at the end.
757    End,
758    /// First and last at the edges, equal gaps between.
759    SpaceBetween,
760    /// Equal space around each child.
761    SpaceAround,
762    /// Equal gaps everywhere, edges included.
763    SpaceEvenly,
764}
765
766/// Where text is cut when it does not fit.
767#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
768#[cfg_attr(feature = "schema", derive(JsonSchema))]
769#[serde(rename_all = "camelCase")]
770pub enum TruncationMode {
771    /// `…end`.
772    Head,
773    /// `sta…end`.
774    Middle,
775    /// `start…` (default).
776    Tail,
777}
778
779/// SF Symbol rendering mode.
780#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
781#[cfg_attr(feature = "schema", derive(JsonSchema))]
782#[serde(rename_all = "camelCase")]
783pub enum SymbolRendering {
784    /// One color (default).
785    Monochrome,
786    /// One color, layered opacity.
787    Hierarchical,
788    /// Up to three `paletteColors`.
789    Palette,
790    /// The symbol's own colors.
791    Multicolor,
792}
793
794/// Button look.
795#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
796#[cfg_attr(feature = "schema", derive(JsonSchema))]
797#[serde(rename_all = "camelCase")]
798pub enum ButtonVariant {
799    /// Tinted rounded background (default).
800    Bordered,
801    /// Filled with `backgroundColor` / accent, white label.
802    Prominent,
803    /// Label only.
804    Plain,
805    /// Outline in the label color, no fill.
806    Outline,
807}
808
809/// Toggle look.
810#[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 (default).
815    Switch,
816    /// Checkbox.
817    Checkbox,
818}
819
820/// Line / area curve.
821#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
822#[cfg_attr(feature = "schema", derive(JsonSchema))]
823#[serde(rename_all = "camelCase")]
824pub enum ChartInterpolation {
825    /// Straight segments (default).
826    Linear,
827    /// Smooth curve through every point.
828    Monotone,
829    /// Steps.
830    Step,
831}
832
833/// Content change animation hint (`transition`).
834#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
835#[cfg_attr(feature = "schema", derive(JsonSchema))]
836#[serde(rename_all = "camelCase")]
837pub enum Transition {
838    /// Swap without animation.
839    None,
840    /// Cross-fade.
841    Opacity,
842    /// Push the new content in from the bottom.
843    Slide,
844    /// Roll changed digits (`contentTransition(.numericText())`).
845    Numeric,
846}