1use serde::{Deserialize, Serialize};
17
18use crate::model::{NEUTRAL_COLOUR, UnstratifiedStyle, style_limits};
19
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct StyleDecl {
24 pub id: String,
26 pub label_key: String,
28 pub label: String,
30 pub params: Vec<ParamDecl>,
32 #[serde(default)]
38 pub suggested: bool,
39}
40
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ParamDecl {
45 pub id: String,
47 pub label_key: String,
49 pub label: String,
51 #[serde(flatten)]
53 pub kind: ParamKind,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "camelCase")]
60pub enum ParamKind {
61 Colour {
63 default: String,
65 },
66 Number {
68 unit: String,
70 min: f64,
72 max: f64,
74 step: f64,
76 default: f64,
78 },
79 Choice {
81 values: Vec<ChoiceValue>,
83 default: String,
85 },
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct ChoiceValue {
92 pub value: String,
94 pub label_key: String,
96 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
144pub 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 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
211pub 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}