Skip to main content

energyml_section/
style.rs

1//! The styles the portions of a well with no strata information can be drawn in,
2//! **declared** (several styles are offered: fixed colour, texture, dashes…, and the front end
3//! must adapt dynamically to the styles proposed).
4//!
5//! The house rule - the operations' slots, the providers' filters - *what the
6//! library declares **is** the front end's form*: [`declared`] lists each style, its
7//! parameters and their kinds, and the front end builds a select and one control per
8//! parameter from it, spelling no style and no parameter. A style added here
9//! (a variant of [`UnstratifiedStyle`] and a [`StyleDecl`]) appears in the front end
10//! with no page change.
11//!
12//! The form's answer **is** the style's JSON: `{ "kind": <id>, <param id>: <value>, … }`,
13//! which `serde` reads into [`UnstratifiedStyle`]. A test holds the declared list
14//! against the enum (defaults, ranges), so the two cannot drift.
15
16use serde::{Deserialize, Serialize};
17
18use crate::model::{NEUTRAL_COLOUR, UnstratifiedStyle, style_limits};
19
20/// One style: what the user picks.
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct StyleDecl {
24    /// The `kind` of the style's JSON.
25    pub id: String,
26    /// The key the front end translates for the label (`sectionView.unstratified.style.dashed`).
27    pub label_key: String,
28    /// The label to show when the front end has no translation for the key.
29    pub label: String,
30    /// Its parameters, in the order the form shows them.
31    pub params: Vec<ParamDecl>,
32    /// The style a front end offers a **new** view - hatched
33    /// texture for the portions without strata. The library's own default
34    /// stays the first of the list (what a drawing has without asking: the CLI,
35    /// the versioned drawings); this is the face's suggestion, said here so that
36    /// the front end spells no style. One at most.
37    #[serde(default)]
38    pub suggested: bool,
39}
40
41/// One parameter of a style.
42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ParamDecl {
45    /// The field of the style's JSON.
46    pub id: String,
47    /// The key the front end translates for the label.
48    pub label_key: String,
49    /// The label to show when the front end has no translation for the key.
50    pub label: String,
51    /// What it is, and its default.
52    #[serde(flatten)]
53    pub kind: ParamKind,
54}
55
56/// What a parameter holds. The front end renders one control per kind; a kind it does
57/// not know is shown as a plain text field of the default's JSON, never dropped.
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "camelCase")]
60pub enum ParamKind {
61    /// A colour (`#rrggbb`).
62    Colour {
63        /// Its default.
64        default: String,
65    },
66    /// A number.
67    Number {
68        /// The unit, for the label (`px`, `°`).
69        unit: String,
70        /// The least value.
71        min: f64,
72        /// The greatest value.
73        max: f64,
74        /// The step of the control.
75        step: f64,
76        /// Its default.
77        default: f64,
78    },
79    /// One of a list of words.
80    Choice {
81        /// The values, in the order the select shows them.
82        values: Vec<ChoiceValue>,
83        /// The default's `value`.
84        default: String,
85    },
86}
87
88/// One value of a [`ParamKind::Choice`].
89#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct ChoiceValue {
92    /// What the JSON holds.
93    pub value: String,
94    /// The key the front end translates for the label.
95    pub label_key: String,
96    /// The label to show when the front end has no translation for the key.
97    pub label: String,
98}
99
100fn style(id: &str, label: &str, params: Vec<ParamDecl>) -> StyleDecl {
101    StyleDecl {
102        id: id.to_string(),
103        label_key: format!("sectionView.unstratified.style.{id}"),
104        label: label.to_string(),
105        params,
106        suggested: false,
107    }
108}
109
110fn param(id: &str, label: &str, kind: ParamKind) -> ParamDecl {
111    ParamDecl {
112        id: id.to_string(),
113        label_key: format!("sectionView.unstratified.param.{id}"),
114        label: label.to_string(),
115        kind,
116    }
117}
118
119fn colour() -> ParamDecl {
120    param(
121        "colour",
122        "Colour",
123        ParamKind::Colour {
124            default: NEUTRAL_COLOUR.to_string(),
125        },
126    )
127}
128
129fn length(id: &str, label: &str, default: f64) -> ParamDecl {
130    let (min, max) = style_limits::LENGTH;
131    param(
132        id,
133        label,
134        ParamKind::Number {
135            unit: "px".into(),
136            min,
137            max,
138            step: 1.0,
139            default,
140        },
141    )
142}
143
144/// The styles the portions with no strata can be drawn in, and their parameters.
145/// The **first** is the default's (the one a drawing has without asking), so a
146/// front end that starts from the list starts from what the library draws.
147pub fn declared() -> Vec<StyleDecl> {
148    let (smin, smax) = style_limits::SPACING;
149    let (amin, amax) = style_limits::ANGLE;
150    vec![
151        style("solid", "Solid line", vec![colour()]),
152        style(
153            "dashed",
154            "Dashed line",
155            vec![colour(), length("dash", "Dash", 6.0), length("gap", "Gap", 4.0)],
156        ),
157        StyleDecl {
158            suggested: true,
159            ..style(
160                "hatched",
161                "Hatched texture",
162                vec![
163                    colour(),
164                    param(
165                        "spacing",
166                        "Spacing",
167                        ParamKind::Number {
168                            unit: "px".into(),
169                            min: smin,
170                            max: smax,
171                            step: 1.0,
172                            default: 4.0,
173                        },
174                    ),
175                    param(
176                        "angle",
177                        "Angle",
178                        ParamKind::Number {
179                            unit: "°".into(),
180                            min: amin,
181                            max: amax,
182                            step: 5.0,
183                            default: 45.0,
184                        },
185                    ),
186                ],
187            )
188        },
189        style("hidden", "Not drawn", Vec::new()),
190    ]
191}
192
193impl StyleDecl {
194    /// The style this declaration gives with every parameter at its default - the
195    /// JSON the front end starts a choice from, read back through `serde`.
196    pub fn default_style(&self) -> Result<UnstratifiedStyle, serde_json::Error> {
197        let mut object = serde_json::Map::new();
198        object.insert("kind".into(), serde_json::Value::String(self.id.clone()));
199        for p in &self.params {
200            let value = match &p.kind {
201                ParamKind::Colour { default } => serde_json::Value::from(default.clone()),
202                ParamKind::Number { default, .. } => serde_json::Value::from(*default),
203                ParamKind::Choice { default, .. } => serde_json::Value::from(default.clone()),
204            };
205            object.insert(p.id.clone(), value);
206        }
207        serde_json::from_value(serde_json::Value::Object(object))
208    }
209}
210
211/// Reads `id` or `id:param=value,param=value` (`dashed:colour=#999999,dash=6,gap=4`)
212/// into a style, against [`declared`]: a parameter left out takes its default, an
213/// unknown style or parameter is refused **naming what exists**. The command line
214/// asks this; it holds no list of its own.
215pub fn parse(text: &str) -> Result<UnstratifiedStyle, String> {
216    let (id, rest) = text.split_once(':').map_or((text, ""), |(a, b)| (a, b));
217    let styles = declared();
218    let Some(style) = styles.iter().find(|s| s.id == id.trim()) else {
219        let names: Vec<&str> = styles.iter().map(|s| s.id.as_str()).collect();
220        return Err(format!(
221            "unknown style `{}`: one of {}",
222            id.trim(),
223            names.join(", ")
224        ));
225    };
226    let mut object = serde_json::Map::new();
227    object.insert("kind".into(), serde_json::Value::String(style.id.clone()));
228    for p in &style.params {
229        let value = match &p.kind {
230            ParamKind::Colour { default } | ParamKind::Choice { default, .. } => {
231                serde_json::Value::from(default.clone())
232            }
233            ParamKind::Number { default, .. } => serde_json::Value::from(*default),
234        };
235        object.insert(p.id.clone(), value);
236    }
237    for pair in rest.split(',').map(str::trim).filter(|p| !p.is_empty()) {
238        let (name, value) = pair
239            .split_once('=')
240            .ok_or_else(|| format!("`{pair}`: expected name=value"))?;
241        let Some(p) = style.params.iter().find(|p| p.id == name.trim()) else {
242            let names: Vec<&str> = style.params.iter().map(|p| p.id.as_str()).collect();
243            return Err(format!(
244                "style `{}` has no parameter `{}`: it has [{}]",
245                style.id,
246                name.trim(),
247                names.join(", ")
248            ));
249        };
250        let value = match &p.kind {
251            ParamKind::Number { .. } => serde_json::Value::from(
252                value
253                    .trim()
254                    .parse::<f64>()
255                    .map_err(|_| format!("parameter `{}`: `{}` is not a number", p.id, value.trim()))?,
256            ),
257            ParamKind::Colour { .. } | ParamKind::Choice { .. } => {
258                serde_json::Value::from(value.trim().to_string())
259            }
260        };
261        object.insert(p.id.clone(), value);
262    }
263    let parsed: UnstratifiedStyle =
264        serde_json::from_value(serde_json::Value::Object(object)).map_err(|why| why.to_string())?;
265    match parsed.refusal() {
266        Some(why) => Err(format!("style `{}`: {why}", style.id)),
267        None => Ok(parsed),
268    }
269}