Skip to main content

deckcraft_model/
style.rs

1//! Colours, fills, lines and effects.
2
3use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot};
4use serde::{Deserialize, Serialize};
5
6use crate::MediaId;
7
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase", tag = "kind")]
10pub enum ColorBase {
11    Rgb {
12        rgb: Rgba,
13    },
14    Scheme {
15        slot: SchemeSlot,
16    },
17    Preset {
18        name: String,
19    },
20    /// A system colour (`windowText`…) with the value it had when saved.
21    System {
22        name: String,
23        last: Rgba,
24    },
25}
26
27/// A colour as written in a document: a base (RGB, theme slot…) plus transforms.
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ColorRef {
31    pub base: ColorBase,
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub mods: Vec<ColorTransform>,
34}
35
36impl ColorRef {
37    pub fn rgb(c: Rgba) -> Self {
38        ColorRef {
39            base: ColorBase::Rgb { rgb: c },
40            mods: vec![],
41        }
42    }
43    pub fn scheme(slot: SchemeSlot) -> Self {
44        ColorRef {
45            base: ColorBase::Scheme { slot },
46            mods: vec![],
47        }
48    }
49    pub fn with(mut self, m: ColorTransform) -> Self {
50        self.mods.push(m);
51        self
52    }
53    /// Resolve against a colour scheme; `ph` substitutes `phClr` (style matrix colour).
54    pub fn resolve(&self, scheme: &ColorScheme, ph: Option<Rgba>) -> Rgba {
55        let base = match &self.base {
56            ColorBase::Rgb { rgb } => *rgb,
57            ColorBase::Scheme {
58                slot: SchemeSlot::PhClr,
59            } => ph.unwrap_or_else(|| scheme.get(SchemeSlot::Accent1)),
60            ColorBase::Scheme { slot } => scheme.get(*slot),
61            ColorBase::Preset { name } => deckcraft_color::preset(name).unwrap_or(Rgba::BLACK),
62            ColorBase::System { last, .. } => *last,
63        };
64        deckcraft_color::apply(base, &self.mods)
65    }
66    /// Alpha (0–1) from an `alpha` transform, if any.
67    pub fn alpha(&self) -> f64 {
68        self.mods.iter().fold(1.0, |a, m| match m {
69            ColorTransform::Alpha(v) => (*v as f64 / 100_000.0).clamp(0.0, 1.0),
70            _ => a,
71        })
72    }
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct GradientStop {
78    /// 0–1.
79    pub pos: f64,
80    pub color: ColorRef,
81}
82
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", tag = "kind")]
85pub enum GradientShape {
86    /// Angle in degrees clockwise from left→right.
87    Linear {
88        angle: f64,
89        #[serde(default)]
90        scaled: bool,
91    },
92    /// `circle`, `rect` or `shape` path gradient; `focus` is the fill-to rect as fractions (l, t, r, b).
93    Path { path: String, focus: [f64; 4] },
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct Gradient {
99    pub stops: Vec<GradientStop>,
100    pub shape: GradientShape,
101    #[serde(default = "crate::yes")]
102    pub rotate_with_shape: bool,
103}
104
105/// How a picture fills its box: stretched into a (fractional) inset rect, or tiled.
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase", tag = "kind")]
108pub enum PictureMode {
109    Stretch {
110        #[serde(default)]
111        fill_rect: [f64; 4],
112    },
113    Tile {
114        tx: f64,
115        ty: f64,
116        sx: f64,
117        sy: f64,
118        #[serde(default)]
119        flip: String,
120        #[serde(default)]
121        align: String,
122    },
123}
124
125impl Default for PictureMode {
126    fn default() -> Self {
127        PictureMode::Stretch {
128            fill_rect: [0.0; 4],
129        }
130    }
131}
132
133#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
134#[serde(default, rename_all = "camelCase")]
135pub struct PictureFill {
136    pub media: MediaId,
137    /// Source crop as fractions of the image (left, top, right, bottom); negative values pad.
138    pub crop: [f64; 4],
139    pub mode: PictureMode,
140    /// 0–1 opacity of the picture.
141    pub alpha: Option<f64>,
142    pub adjust: PictureAdjust,
143}
144
145/// Picture corrections and colour (Picture Format ▸ Corrections / Color / Transparency).
146#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
147#[serde(default, rename_all = "camelCase")]
148pub struct PictureAdjust {
149    /// -1..1
150    pub brightness: f64,
151    /// -1..1
152    pub contrast: f64,
153    /// 0..4 (1 = unchanged)
154    pub saturation: Option<f64>,
155    pub grayscale: bool,
156    /// Recolour duotone (dark, light).
157    pub duotone: Option<(ColorRef, ColorRef)>,
158    /// Make a colour transparent (Set Transparent Color).
159    pub clear_color: Option<Rgba>,
160    pub sharpen: f64,
161}
162
163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct PatternFill {
166    pub preset: String,
167    pub fg: ColorRef,
168    pub bg: ColorRef,
169}
170
171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase", tag = "kind")]
173pub enum Fill {
174    None,
175    Solid {
176        color: ColorRef,
177    },
178    Gradient(Gradient),
179    Picture(PictureFill),
180    Pattern(PatternFill),
181    /// Use the containing group's fill.
182    Group,
183    /// Use the slide background (`useBgFill`).
184    Background,
185}
186
187impl Fill {
188    pub fn solid(c: ColorRef) -> Self {
189        Fill::Solid { color: c }
190    }
191    pub fn is_none(&self) -> bool {
192        matches!(self, Fill::None)
193    }
194}
195
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub enum LineCap {
199    #[default]
200    Flat,
201    Round,
202    Square,
203}
204
205#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub enum LineJoin {
208    #[default]
209    Round,
210    Bevel,
211    Miter,
212}
213
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub enum Compound {
217    #[default]
218    Single,
219    Double,
220    ThickThin,
221    ThinThick,
222    Triple,
223}
224
225impl Compound {
226    pub fn xml(self) -> &'static str {
227        match self {
228            Compound::Single => "sng",
229            Compound::Double => "dbl",
230            Compound::ThickThin => "thickThin",
231            Compound::ThinThick => "thinThick",
232            Compound::Triple => "tri",
233        }
234    }
235    pub fn from_xml(s: &str) -> Self {
236        match s {
237            "dbl" => Compound::Double,
238            "thickThin" => Compound::ThickThin,
239            "thinThick" => Compound::ThinThick,
240            "tri" => Compound::Triple,
241            _ => Compound::Single,
242        }
243    }
244}
245
246/// Dash style: a preset name (`solid`, `dot`, `dash`, `lgDash`, `dashDot`, `lgDashDot`,
247/// `lgDashDotDot`, `sysDash`, `sysDot`, `sysDashDot`, `sysDashDotDot`) or custom (dash, space) pairs
248/// in multiples of the line width.
249#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase", tag = "kind")]
251pub enum Dash {
252    Preset { name: String },
253    Custom { pattern: Vec<(f64, f64)> },
254}
255
256impl Dash {
257    pub const PRESETS: [&'static str; 11] = [
258        "solid",
259        "sysDot",
260        "sysDash",
261        "dash",
262        "dashDot",
263        "lgDash",
264        "lgDashDot",
265        "lgDashDotDot",
266        "dot",
267        "sysDashDot",
268        "sysDashDotDot",
269    ];
270    /// Dash/space lengths in multiples of the line width.
271    pub fn pattern(&self) -> Vec<f64> {
272        match self {
273            Dash::Custom { pattern } => pattern.iter().flat_map(|(d, s)| [*d, *s]).collect(),
274            Dash::Preset { name } => match name.as_str() {
275                "dot" => vec![1.0, 3.0],
276                "dash" => vec![4.0, 3.0],
277                "lgDash" => vec![8.0, 3.0],
278                "dashDot" => vec![4.0, 3.0, 1.0, 3.0],
279                "lgDashDot" => vec![8.0, 3.0, 1.0, 3.0],
280                "lgDashDotDot" => vec![8.0, 3.0, 1.0, 3.0, 1.0, 3.0],
281                "sysDash" => vec![3.0, 1.0],
282                "sysDot" => vec![1.0, 1.0],
283                "sysDashDot" => vec![3.0, 1.0, 1.0, 1.0],
284                "sysDashDotDot" => vec![3.0, 1.0, 1.0, 1.0, 1.0, 1.0],
285                _ => vec![],
286            },
287        }
288    }
289}
290
291/// Arrowhead at a line end.
292#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub struct LineEnd {
295    /// `none`, `triangle`, `stealth`, `diamond`, `oval`, `arrow`.
296    pub kind: String,
297    /// `sm`, `med`, `lg`.
298    #[serde(default = "med")]
299    pub w: String,
300    #[serde(default = "med")]
301    pub len: String,
302}
303
304fn med() -> String {
305    "med".into()
306}
307
308#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
309#[serde(default, rename_all = "camelCase")]
310pub struct Line {
311    pub fill: Option<Fill>,
312    /// Width in points.
313    pub width: Option<f64>,
314    pub dash: Option<Dash>,
315    pub cap: Option<LineCap>,
316    pub join: Option<LineJoin>,
317    pub compound: Option<Compound>,
318    pub head: Option<LineEnd>,
319    pub tail: Option<LineEnd>,
320}
321
322impl Line {
323    pub fn solid(c: ColorRef, width: f64) -> Self {
324        Line {
325            fill: Some(Fill::solid(c)),
326            width: Some(width),
327            ..Default::default()
328        }
329    }
330    pub fn none() -> Self {
331        Line {
332            fill: Some(Fill::None),
333            ..Default::default()
334        }
335    }
336    /// Fill unset fields from `parent`.
337    pub fn inherit(&mut self, parent: &Line) {
338        if self.fill.is_none() {
339            self.fill.clone_from(&parent.fill);
340        }
341        self.width = self.width.or(parent.width);
342        if self.dash.is_none() {
343            self.dash.clone_from(&parent.dash);
344        }
345        self.cap = self.cap.or(parent.cap);
346        self.join = self.join.or(parent.join);
347        self.compound = self.compound.or(parent.compound);
348        if self.head.is_none() {
349            self.head.clone_from(&parent.head);
350        }
351        if self.tail.is_none() {
352            self.tail.clone_from(&parent.tail);
353        }
354    }
355}
356
357#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct Shadow {
360    pub color: ColorRef,
361    /// Blur radius, points.
362    pub blur: f64,
363    /// Distance, points.
364    pub dist: f64,
365    /// Direction, degrees (0 = right, 90 = down).
366    pub dir: f64,
367    #[serde(default)]
368    pub inner: bool,
369    /// Scale (1 = 100%) for perspective shadows.
370    #[serde(default = "one")]
371    pub sx: f64,
372    #[serde(default = "one")]
373    pub sy: f64,
374    #[serde(default)]
375    pub kx: f64,
376    #[serde(default)]
377    pub ky: f64,
378    #[serde(default)]
379    pub align: String,
380    #[serde(default = "crate::yes")]
381    pub rotate_with_shape: bool,
382}
383
384fn one() -> f64 {
385    1.0
386}
387
388#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
389#[serde(rename_all = "camelCase")]
390pub struct Glow {
391    pub color: ColorRef,
392    pub radius: f64,
393}
394
395#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct Reflection {
398    pub blur: f64,
399    pub start_alpha: f64,
400    pub end_alpha: f64,
401    pub end_pos: f64,
402    pub dist: f64,
403}
404
405#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
406#[serde(default, rename_all = "camelCase")]
407pub struct Effects {
408    pub outer_shadow: Option<Shadow>,
409    pub inner_shadow: Option<Shadow>,
410    pub glow: Option<Glow>,
411    /// Soft edges radius in points.
412    pub soft_edge: Option<f64>,
413    pub reflection: Option<Reflection>,
414    /// Bevel preset name for 3-D format (rendered as an approximation).
415    pub bevel: Option<String>,
416    /// Unmodelled 3-D XML kept for round-trip (`scene3d`, `sp3d`).
417    pub raw3d: Option<String>,
418    /// `a:fillOverlay`: a fill blended over the shape's fill (`blend`: `over`, `mult`, `screen`,
419    /// `darken`, `lighten`). Only solid and gradient overlays are kept.
420    pub fill_overlay: Option<FillOverlay>,
421}
422
423#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct FillOverlay {
426    pub blend: String,
427    pub fill: Fill,
428}
429
430impl Effects {
431    pub fn is_empty(&self) -> bool {
432        self.outer_shadow.is_none()
433            && self.inner_shadow.is_none()
434            && self.glow.is_none()
435            && self.soft_edge.is_none()
436            && self.reflection.is_none()
437            && self.bevel.is_none()
438            && self.fill_overlay.is_none()
439    }
440}
441
442/// A shape's theme style references (`p:style`): which fill/line/effect of the theme's format scheme
443/// to use, with which colour, and the theme font and colour for its text.
444#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct ShapeStyle {
447    pub line_ref: (u32, ColorRef),
448    pub fill_ref: (u32, ColorRef),
449    pub effect_ref: (u32, ColorRef),
450    /// `major`, `minor` or `none`, with the text colour.
451    pub font_ref: (String, Option<ColorRef>),
452}
453
454impl ShapeStyle {
455    /// The default for newly drawn shapes: accent 1 fill, darker accent 1 outline, light text.
456    pub fn accent(slot: SchemeSlot) -> Self {
457        ShapeStyle {
458            line_ref: (2, ColorRef::scheme(slot).with(ColorTransform::Shade(50000))),
459            fill_ref: (1, ColorRef::scheme(slot)),
460            effect_ref: (0, ColorRef::scheme(slot)),
461            font_ref: ("minor".into(), Some(ColorRef::scheme(SchemeSlot::Lt1))),
462        }
463    }
464    /// The default for lines/connectors.
465    pub fn line(slot: SchemeSlot) -> Self {
466        ShapeStyle {
467            line_ref: (1, ColorRef::scheme(slot)),
468            fill_ref: (0, ColorRef::scheme(slot)),
469            effect_ref: (0, ColorRef::scheme(slot)),
470            font_ref: ("minor".into(), Some(ColorRef::scheme(SchemeSlot::Tx1))),
471        }
472    }
473}