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}
46
47/// Color value — hex string, semantic name, or adaptive `{ light, dark }` pair.
48///
49/// Semantic names: `"label"`, `"secondaryLabel"`, `"systemBackground"`,
50/// `"secondarySystemBackground"`, `"accent"`, `"separator"`.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(JsonSchema))]
53#[serde(untagged)]
54pub enum ColorValue {
55    /// Hex string or semantic color name.
56    Solid(String),
57    /// Distinct colors for light and dark appearance.
58    Adaptive {
59        /// Color used in light appearance.
60        light: String,
61        /// Color used in dark appearance.
62        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/// Clip shape for content masking.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(JsonSchema))]
81#[serde(rename_all = "camelCase")]
82pub enum ClipShape {
83    /// `circle`.
84    Circle,
85    /// `capsule`.
86    Capsule,
87    /// `rectangle`.
88    Rectangle,
89}
90
91/// Semantic text style — respects Dynamic Type / accessibility settings.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[cfg_attr(feature = "schema", derive(JsonSchema))]
94#[serde(rename_all = "camelCase")]
95pub enum TextStyle {
96    /// `large title`.
97    LargeTitle,
98    /// `title`.
99    Title,
100    /// `title2`.
101    Title2,
102    /// `title3`.
103    Title3,
104    /// `headline`.
105    Headline,
106    /// `subheadline`.
107    Subheadline,
108    /// `body`.
109    Body,
110    /// `callout`.
111    Callout,
112    /// `footnote`.
113    Footnote,
114    /// `caption`.
115    Caption,
116    /// `caption2`.
117    Caption2,
118}
119
120/// Background: solid color string, adaptive pair, gradient, or material blur.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[cfg_attr(feature = "schema", derive(JsonSchema))]
123#[serde(untagged)]
124pub enum BackgroundValue {
125    /// Hex string or semantic color name.
126    Solid(String),
127    /// Linear, radial, or angular gradient.
128    Gradient(GradientConfig),
129    /// Distinct colors for light and dark appearance.
130    Adaptive {
131        /// Color used in light appearance.
132        light: String,
133        /// Color used in dark appearance.
134        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/// Linear, radial, or angular gradient background.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[cfg_attr(feature = "schema", derive(JsonSchema))]
153#[serde(rename_all = "camelCase")]
154pub struct GradientConfig {
155    /// `"linear"`, `"radial"`, or `"angular"`
156    #[serde(rename = "gradientType")]
157    pub gradient_type: GradientType,
158    /// Stop colors, in order.
159    pub colors: Vec<String>,
160    /// Direction for linear gradients
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub direction: Option<GradientDirection>,
163}
164
165/// Gradient shape.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "schema", derive(JsonSchema))]
168#[serde(rename_all = "camelCase")]
169pub enum GradientType {
170    /// `linear`.
171    Linear,
172    /// `radial`.
173    Radial,
174    /// `angular`.
175    Angular,
176}
177
178/// Direction for linear gradients.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[cfg_attr(feature = "schema", derive(JsonSchema))]
181#[serde(rename_all = "camelCase")]
182pub enum GradientDirection {
183    /// `top to bottom`.
184    TopToBottom,
185    /// `bottom to top`.
186    BottomToTop,
187    /// `leading to trailing`.
188    LeadingToTrailing,
189    /// `trailing to leading`.
190    TrailingToLeading,
191    /// `top leading to bottom trailing`.
192    TopLeadingToBottomTrailing,
193    /// `top trailing to bottom leading`.
194    TopTrailingToBottomLeading,
195}
196
197/// Drop shadow configuration.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[cfg_attr(feature = "schema", derive(JsonSchema))]
200#[serde(rename_all = "camelCase")]
201pub struct ShadowConfig {
202    /// Shadow color (hex string or semantic name).
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub color: Option<String>,
205    /// Blur radius in points.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub radius: Option<f64>,
208    /// Horizontal offset in points.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub x: Option<f64>,
211    /// Vertical offset in points.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub y: Option<f64>,
214}
215
216/// Inset padding — a single uniform value or per-edge overrides.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[cfg_attr(feature = "schema", derive(JsonSchema))]
219#[serde(untagged)]
220pub enum PaddingValue {
221    /// Same padding on every edge.
222    Uniform(f64),
223    /// Independent padding per edge.
224    Edges {
225        /// Top edge padding.
226        #[serde(default, skip_serializing_if = "Option::is_none")]
227        top: Option<f64>,
228        /// Bottom edge padding.
229        #[serde(default, skip_serializing_if = "Option::is_none")]
230        bottom: Option<f64>,
231        /// Leading (left in LTR) edge padding.
232        #[serde(default, skip_serializing_if = "Option::is_none")]
233        leading: Option<f64>,
234        /// Trailing (right in LTR) edge padding.
235        #[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/// Explicit width / height / max-size constraints for an element.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260#[cfg_attr(feature = "schema", derive(JsonSchema))]
261#[serde(rename_all = "camelCase")]
262pub struct FrameConfig {
263    /// Fixed width in points.
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub width: Option<f64>,
266    /// Fixed height in points.
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub height: Option<f64>,
269    /// Maximum width — a fixed point value or `"infinity"`.
270    #[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
271    pub max_width: Option<FrameDimension>,
272    /// Maximum height — a fixed point value or `"infinity"`.
273    #[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
274    pub max_height: Option<FrameDimension>,
275}
276
277/// A frame dimension — either a fixed point value or `"infinity"`.
278#[derive(Debug, Clone)]
279pub enum FrameDimension {
280    /// Fixed point value.
281    Fixed(f64),
282    /// `"infinity"`.
283    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/// Border color and width.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369#[cfg_attr(feature = "schema", derive(JsonSchema))]
370#[serde(rename_all = "camelCase")]
371pub struct BorderConfig {
372    /// Border color (hex string or semantic name).
373    pub color: String,
374    /// Border width in points. Default `1.0`.
375    #[serde(default = "default_border_width")]
376    pub width: f64,
377}
378
379fn default_border_width() -> f64 {
380    1.0
381}
382
383/// Font weight.
384#[derive(Debug, Clone, Serialize, Deserialize)]
385#[cfg_attr(feature = "schema", derive(JsonSchema))]
386#[serde(rename_all = "camelCase")]
387pub enum FontWeight {
388    /// `ultralight`.
389    Ultralight,
390    /// `thin`.
391    Thin,
392    /// `light`.
393    Light,
394    /// `regular`.
395    Regular,
396    /// `medium`.
397    Medium,
398    /// `semibold`.
399    Semibold,
400    /// `bold`.
401    Bold,
402    /// `heavy`.
403    Heavy,
404    /// `black`.
405    Black,
406}
407
408/// Font design (default, monospaced, rounded, serif).
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[cfg_attr(feature = "schema", derive(JsonSchema))]
411#[serde(rename_all = "camelCase")]
412pub enum FontDesign {
413    /// `default`.
414    Default,
415    /// `monospaced`.
416    Monospaced,
417    /// `rounded`.
418    Rounded,
419    /// `serif`.
420    Serif,
421}
422
423/// Text alignment within the line.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425#[cfg_attr(feature = "schema", derive(JsonSchema))]
426#[serde(rename_all = "camelCase")]
427pub enum TextAlignment {
428    /// `leading`.
429    Leading,
430    /// `center`.
431    Center,
432    /// `trailing`.
433    Trailing,
434}
435
436/// Horizontal alignment of children within a stack.
437#[derive(Debug, Clone, Serialize, Deserialize)]
438#[cfg_attr(feature = "schema", derive(JsonSchema))]
439#[serde(rename_all = "camelCase")]
440pub enum HorizontalAlignment {
441    /// `leading`.
442    Leading,
443    /// `center`.
444    Center,
445    /// `trailing`.
446    Trailing,
447}
448
449/// Vertical alignment of children within a stack.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451#[cfg_attr(feature = "schema", derive(JsonSchema))]
452#[serde(rename_all = "camelCase")]
453pub enum VerticalAlignment {
454    /// `top`.
455    Top,
456    /// `center`.
457    Center,
458    /// `bottom`.
459    Bottom,
460}
461
462/// How an image fills its frame.
463#[derive(Debug, Clone, Serialize, Deserialize)]
464#[cfg_attr(feature = "schema", derive(JsonSchema))]
465#[serde(rename_all = "camelCase")]
466pub enum ContentMode {
467    /// `fit`.
468    Fit,
469    /// `fill`.
470    Fill,
471}
472
473/// Progress indicator style.
474#[derive(Debug, Clone, Serialize, Deserialize)]
475#[cfg_attr(feature = "schema", derive(JsonSchema))]
476#[serde(rename_all = "camelCase")]
477pub enum ProgressStyle {
478    /// `linear`.
479    Linear,
480    /// `circular`.
481    Circular,
482}
483
484/// Gauge visual style.
485#[derive(Debug, Clone, Serialize, Deserialize)]
486#[cfg_attr(feature = "schema", derive(JsonSchema))]
487#[serde(rename_all = "camelCase")]
488pub enum GaugeStyle {
489    /// `circular`.
490    Circular,
491    /// `linear`.
492    Linear,
493}
494
495/// Date / relative-time display style.
496#[derive(Debug, Clone, Serialize, Deserialize)]
497#[cfg_attr(feature = "schema", derive(JsonSchema))]
498#[serde(rename_all = "camelCase")]
499pub enum DateStyle {
500    /// `time`.
501    Time,
502    /// `date`.
503    Date,
504    /// `relative`.
505    Relative,
506    /// `offset`.
507    Offset,
508    /// `timer`.
509    Timer,
510}
511
512/// Chart kind.
513#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514#[cfg_attr(feature = "schema", derive(JsonSchema))]
515#[serde(rename_all = "camelCase")]
516pub enum ChartType {
517    /// `bar`.
518    #[default]
519    Bar,
520    /// `line`.
521    Line,
522    /// `area`.
523    Area,
524    /// `pie`.
525    Pie,
526}
527
528/// Shape kind for [`crate::models::ShapeElement`].
529#[derive(Debug, Clone, Serialize, Deserialize, Default)]
530#[cfg_attr(feature = "schema", derive(JsonSchema))]
531#[serde(rename_all = "camelCase")]
532pub enum ShapeType {
533    /// `circle`.
534    #[default]
535    Circle,
536    /// `capsule`.
537    Capsule,
538    /// `rectangle`.
539    Rectangle,
540}
541
542/// Countdown/countup direction for [`crate::models::TimerElement`].
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[cfg_attr(feature = "schema", derive(JsonSchema))]
545#[serde(rename_all = "camelCase")]
546pub enum TimerCounting {
547    /// `up`.
548    Up,
549    /// `down`.
550    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}