1use std::collections::HashMap;
7
8use i_slint_compiler::langtype;
9use i_slint_core::{
10 graphics::Image,
11 model::{Model, ModelRc},
12 Brush, Color, SharedString, SharedVector,
13};
14
15use crate::Value;
16
17pub trait JsonExt
19where
20 Self: Sized,
21{
22 fn to_json(&self) -> Result<serde_json::Value, String>;
24 fn to_json_string(&self) -> Result<String, String>;
26 fn from_json(t: &langtype::Type, value: &serde_json::Value) -> Result<Self, String>;
28 fn from_json_str(t: &langtype::Type, value: &str) -> Result<Self, String>;
30}
31
32impl JsonExt for crate::Value {
33 fn to_json(&self) -> Result<serde_json::Value, String> {
34 value_to_json(self)
35 }
36
37 fn to_json_string(&self) -> Result<String, String> {
38 value_to_json_string(self)
39 }
40
41 fn from_json(t: &langtype::Type, value: &serde_json::Value) -> Result<Self, String> {
42 value_from_json(t, value)
43 }
44
45 fn from_json_str(t: &langtype::Type, value: &str) -> Result<Self, String> {
46 value_from_json_str(t, value)
47 }
48}
49
50pub fn value_from_json(t: &langtype::Type, v: &serde_json::Value) -> Result<Value, String> {
52 use smol_str::ToSmolStr;
53
54 fn string_to_color(s: &str) -> Option<i_slint_core::Color> {
55 i_slint_compiler::literals::parse_color_literal(s).map(Color::from_argb_encoded)
56 }
57
58 match v {
59 serde_json::Value::Null => Ok(Value::Void),
60 serde_json::Value::Bool(b) => Ok((*b).into()),
61 serde_json::Value::Number(n) => Ok(Value::Number(n.as_f64().unwrap_or(f64::NAN))),
62 serde_json::Value::String(s) => match t {
63 langtype::Type::Enumeration(e) => {
64 let s = if let Some(suffix) = s.strip_prefix(&format!("{}.", e.name)) {
65 suffix.to_smolstr()
66 } else {
67 s.to_smolstr()
68 };
69
70 if e.values.contains(&s) {
71 Ok(Value::EnumerationValue(e.name.to_string(), s.into()))
72 } else {
73 Err(format!("Unexpected value for enum '{}': {}", e.name, s))
74 }
75 }
76 langtype::Type::Color => {
77 if let Some(c) = string_to_color(s) {
78 Ok(Value::Brush(i_slint_core::Brush::SolidColor(c)))
79 } else {
80 Err(format!("Failed to parse color: {s}"))
81 }
82 }
83 langtype::Type::String => Ok(SharedString::from(s.as_str()).into()),
84 langtype::Type::Image => match Image::load_from_path(std::path::Path::new(s)) {
85 Ok(image) => Ok(image.into()),
86 Err(e) => Err(format!("Failed to load image from path: {s}: {e}")),
87 },
88 langtype::Type::Brush => {
89 fn string_to_brush(input: &str) -> Result<i_slint_core::graphics::Brush, String> {
90 fn parse_stops<'a>(
91 it: impl Iterator<Item = &'a str>,
92 ) -> Result<Vec<i_slint_core::graphics::GradientStop>, String>
93 {
94 it.filter(|part| !part.is_empty()).map(|part| {
95 let sub_parts = part.split_whitespace().collect::<Vec<_>>();
96 if sub_parts.len() != 2 {
97 Err("A gradient stop must consist of a color and a position in '%' separated by whitespace".into())
98 } else {
99 let color = string_to_color(sub_parts[0]);
100 let position = {
101 if let Some(percent_value) = sub_parts[1].strip_suffix("%") {
102 percent_value.parse::<f32>().map_err(|_| format!("Could not parse position '{}' as number", sub_parts[1]))
103 } else {
104 Err(format!("The position '{}' does not end in '%'", sub_parts[1]))
105 }
106 };
107
108 match (color, position) {
109 (Some(c), Ok(p)) => Ok(i_slint_core::graphics::GradientStop { color: c, position: p / 100.0}),
110 (_, Err(e)) => Err(e),
111 (None, _) => Err(format!("'{}' is not a color", sub_parts[0])),
112 }
113 }
114 }).collect()
115 }
116
117 let Some(input) = input.strip_suffix(')') else {
118 return Err(format!("No closing ')' in '{input}'"));
119 };
120
121 if let Some(linear) = input.strip_prefix("@linear-gradient(") {
122 let mut split = linear.split(',').map(|p| p.trim());
123
124 let angle = {
125 let Some(angle_part) = split.next() else {
126 return Err(
127 "A linear gradient must start with an angle in 'deg'".into()
128 );
129 };
130
131 angle_part
132 .strip_suffix("deg")
133 .ok_or_else(|| {
134 "A linear brush needs to start with an angle in 'deg'"
135 .to_string()
136 })
137 .and_then(|no| {
138 no.parse::<f32>()
139 .map_err(|_| "Failed to parse angle value".into())
140 })
141 }?;
142
143 Ok(i_slint_core::graphics::LinearGradientBrush::new(
144 angle,
145 parse_stops(split)?.drain(..),
146 )
147 .into())
148 } else if let Some(radial) = input.strip_prefix("@radial-gradient(circle") {
149 let split = radial.split(',').map(|p| p.trim());
150
151 Ok(i_slint_core::graphics::RadialGradientBrush::new_circle(
152 parse_stops(split)?.drain(..),
153 )
154 .into())
155 } else {
156 Err(format!("Could not parse gradient from '{input}'"))
157 }
158 }
159
160 if s.starts_with('#') {
161 if let Some(c) = string_to_color(s) {
162 Ok(Value::Brush(i_slint_core::Brush::SolidColor(c)))
163 } else {
164 Err(format!("Failed to parse color value {s}"))
165 }
166 } else {
167 Ok(Value::Brush(string_to_brush(s)?))
168 }
169 }
170 _ => Err("Value type not supported".into()),
171 },
172 serde_json::Value::Array(array) => match t {
173 langtype::Type::Array(it) => {
174 Ok(Value::Model(ModelRc::new(i_slint_core::model::SharedVectorModel::from(
175 array
176 .iter()
177 .map(|v| value_from_json(it, v))
178 .collect::<Result<SharedVector<Value>, String>>()?,
179 ))))
180 }
181 _ => Err("Got an array where none was expected".into()),
182 },
183 serde_json::Value::Object(obj) => match t {
184 langtype::Type::Struct(s) => Ok(crate::Struct(
185 obj.iter()
186 .map(|(k, v)| {
187 let k = crate::api::normalize_identifier(k);
188 match s.fields.get(&k) {
189 Some(t) => value_from_json(t, v).map(|v| (k, v)),
190 None => Err(format!("Found unknown field in struct: {k}")),
191 }
192 })
193 .collect::<Result<HashMap<smol_str::SmolStr, Value>, _>>()?,
194 )
195 .into()),
196 _ => Err("Got a struct where none was expected".into()),
197 },
198 }
199}
200
201pub fn value_from_json_str(t: &langtype::Type, v: &str) -> Result<Value, String> {
203 let value = serde_json::from_str(v).map_err(|e| format!("Failed to parse JSON: {e}"))?;
204 Value::from_json(t, &value)
205}
206
207pub fn value_to_json(value: &Value) -> Result<serde_json::Value, String> {
209 fn color_to_string(color: &Color) -> String {
210 let a = color.alpha();
211 let r = color.red();
212 let g = color.green();
213 let b = color.blue();
214
215 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
216 }
217
218 fn gradient_to_string_helper<'a>(
219 prefix: String,
220 stops: impl Iterator<Item = &'a i_slint_core::graphics::GradientStop>,
221 ) -> serde_json::Value {
222 let mut gradient = prefix;
223
224 for stop in stops {
225 gradient += &format!(", {} {}%", color_to_string(&stop.color), stop.position * 100.0);
226 }
227
228 gradient += ")";
229
230 serde_json::Value::String(gradient)
231 }
232
233 match value {
234 Value::Void => Ok(serde_json::Value::Null),
235 Value::Bool(b) => Ok((*b).into()),
236 Value::Number(n) => {
237 let r = if *n == n.round() {
238 if *n >= 0.0 {
239 serde_json::Number::from_u128(*n as u128)
240 } else {
241 serde_json::Number::from_i128(*n as i128)
242 }
243 } else {
244 serde_json::Number::from_f64(*n)
245 };
246 if let Some(r) = r {
247 Ok(serde_json::Value::Number(r))
248 } else {
249 Err(format!("Could not convert {n} into a number"))
250 }
251 }
252 Value::EnumerationValue(e, v) => Ok(serde_json::Value::String(format!("{e}.{v}"))),
253 Value::String(shared_string) => Ok(serde_json::Value::String(shared_string.to_string())),
254 Value::Image(image) => {
255 if let Some(p) = image.path() {
256 Ok(serde_json::Value::String(format!("{}", p.to_string_lossy())))
257 } else {
258 Err("Cannot serialize an image without a path".into())
259 }
260 }
261 Value::Model(model_rc) => Ok(serde_json::Value::Array(
262 model_rc.iter().map(|v| v.to_json()).collect::<Result<Vec<_>, _>>()?,
263 )),
264 Value::Struct(s) => Ok(serde_json::Value::Object(
265 s.iter()
266 .map(|(k, v)| v.to_json().map(|v| (k.to_string(), v)))
267 .collect::<Result<serde_json::Map<_, _>, _>>()?,
268 )),
269 Value::Brush(brush) => match brush {
270 Brush::SolidColor(color) => Ok(serde_json::Value::String(color_to_string(color))),
271 Brush::LinearGradient(lg) => Ok(gradient_to_string_helper(
272 format!("@linear-gradient({}deg", lg.angle()),
273 lg.stops(),
274 )),
275 Brush::RadialGradient(rg) => {
276 Ok(gradient_to_string_helper("@radial-gradient(circle".into(), rg.stops()))
277 }
278 _ => Err("Cannot serialize an unknown brush type".into()),
279 },
280 Value::PathData(_) => Err("Cannot serialize path data".into()),
281 Value::EasingCurve(_) => Err("Cannot serialize a easing curve".into()),
282 _ => Err("Cannot serialize an unknown value type".into()),
283 }
284}
285
286pub fn value_to_json_string(value: &Value) -> Result<String, String> {
288 Ok(value_to_json(value)?.to_string())
289}
290
291#[test]
292fn test_from_json() {
293 let v = value_from_json_str(&langtype::Type::Void, "null").unwrap();
294 assert_eq!(v, Value::Void);
295 let v = Value::from_json_str(&langtype::Type::Void, "null").unwrap();
296 assert_eq!(v, Value::Void);
297
298 let v = value_from_json_str(&langtype::Type::Float32, "42.0").unwrap();
299 assert_eq!(v, Value::Number(42.0));
300
301 let v = value_from_json_str(&langtype::Type::Int32, "23").unwrap();
302 assert_eq!(v, Value::Number(23.0));
303
304 let v = value_from_json_str(&langtype::Type::String, "\"a string with \\\\ escape\"").unwrap();
305 assert_eq!(v, Value::String("a string with \\ escape".into()));
306
307 let v = value_from_json_str(&langtype::Type::Color, "\"#0ab0cdff\"").unwrap();
308 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
309 let v = value_from_json_str(&langtype::Type::Brush, "\"#0ab0cdff\"").unwrap();
310 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
311 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
312 let v = value_from_json_str(
313 &langtype::Type::Brush,
314 "\"@linear-gradient(42deg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
315 )
316 .unwrap();
317 assert_eq!(
318 v,
319 Value::Brush(Brush::LinearGradient(i_slint_core::graphics::LinearGradientBrush::new(
320 42.0,
321 vec![
322 i_slint_core::graphics::GradientStop {
323 position: 0.0,
324 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
325 },
326 i_slint_core::graphics::GradientStop {
327 position: 0.5,
328 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
329 },
330 i_slint_core::graphics::GradientStop {
331 position: 1.0,
332 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
333 }
334 ]
335 .drain(..)
336 )))
337 );
338 assert!(value_from_json_str(
339 &langtype::Type::Brush,
340 "\"@linear-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
341 )
342 .is_err());
343 assert!(value_from_json_str(
344 &langtype::Type::Brush,
345 "\"@linear-gradient(#ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
346 )
347 .is_err());
348 assert!(value_from_json_str(
349 &langtype::Type::Brush,
350 "\"@linear-gradient(90turns, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
351 )
352 .is_err());
353 assert!(value_from_json_str(
354 &langtype::Type::Brush,
355 "\"@linear-gradient(xfdeg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
356 )
357 .is_err());
358 assert!(value_from_json_str(
359 &langtype::Type::Brush,
360 "\"@linear-gradient(90deg, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
361 )
362 .is_err());
363 assert!(value_from_json_str(
364 &langtype::Type::Brush,
365 "\"@linear-gradient(90deg, #ff0000ff 0, #00ff00ff 50%, #0000ffff 100%)\""
366 )
367 .is_err());
368
369 let v = value_from_json_str(
370 &langtype::Type::Brush,
371 "\"@radial-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
372 )
373 .unwrap();
374 assert_eq!(
375 v,
376 Value::Brush(Brush::RadialGradient(
377 i_slint_core::graphics::RadialGradientBrush::new_circle(
378 vec![
379 i_slint_core::graphics::GradientStop {
380 position: 0.0,
381 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
382 },
383 i_slint_core::graphics::GradientStop {
384 position: 0.5,
385 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
386 },
387 i_slint_core::graphics::GradientStop {
388 position: 1.0,
389 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
390 }
391 ]
392 .drain(..)
393 )
394 ))
395 );
396 assert!(value_from_json_str(
397 &langtype::Type::Brush,
398 "\"@radial-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
399 )
400 .is_err());
401 assert!(value_from_json_str(
402 &langtype::Type::Brush,
403 "\"@radial-gradient(circle, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
404 )
405 .is_err());
406 assert!(value_from_json_str(
407 &langtype::Type::Brush,
408 "\"@radial-gradient(circle, #ff0000ff 1000px, #00ff00ff 50%, #0000ffff 100%)\""
409 )
410 .is_err());
411 assert!(value_from_json_str(
412 &langtype::Type::Brush,
413 "\"@radial-gradient(circle, #ff0000ff 0% #00ff00ff 50%, #0000ffff 100%)\""
414 )
415 .is_err());
416 assert!(value_from_json_str(
417 &langtype::Type::Brush,
418 "\"@radial-gradient(circle, #ff0000ff, #0000ffff)\""
419 )
420 .is_err());
421
422 assert!(value_from_json_str(
423 &langtype::Type::Brush,
424 "\"@radial-gradient(conical, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
425 )
426 .is_err());
427
428 assert!(value_from_json_str(
429 &langtype::Type::Brush,
430 "\"@other-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
431 )
432 .is_err());
433}
434
435#[test]
436fn test_to_json() {
437 let v = value_to_json_string(&Value::Void).unwrap();
438 assert_eq!(&v, "null");
439 let v = Value::Void.to_json_string().unwrap();
440 assert_eq!(&v, "null");
441
442 let v = value_to_json_string(&Value::Number(23.0)).unwrap();
443 assert_eq!(&v, "23");
444
445 let v = value_to_json_string(&Value::Number(4.2)).unwrap();
446 assert_eq!(&v, "4.2");
447
448 let v = value_to_json_string(&Value::EnumerationValue("Foo".to_string(), "bar".to_string()))
449 .unwrap();
450 assert_eq!(&v, "\"Foo.bar\"");
451
452 let v = value_to_json_string(&Value::String("Hello World with \\ escaped".into())).unwrap();
453 assert_eq!(&v, "\"Hello World with \\\\ escaped\"");
454
455 let buffer = i_slint_core::graphics::SharedPixelBuffer::new(2, 2);
457 assert!(value_to_json_string(&Value::Image(Image::from_rgb8(buffer))).is_err());
458
459 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
461 .join("../../logo/MadeWithSlint-logo-dark.png")
462 .canonicalize()
463 .unwrap();
464 let v = value_to_json_string(&Value::Image(Image::load_from_path(&path).unwrap())).unwrap();
465 let path = path.to_string_lossy().replace("\\", "\\\\");
467 assert_eq!(v, format!("\"{path}\""));
468
469 let v = value_to_json_string(&Value::Bool(true)).unwrap();
470 assert_eq!(&v, "true");
471
472 let v = value_to_json_string(&Value::Bool(false)).unwrap();
473 assert_eq!(&v, "false");
474
475 let model: ModelRc<Value> = std::rc::Rc::new(i_slint_core::model::VecModel::from(vec![
476 Value::Bool(true),
477 Value::Bool(false),
478 ]))
479 .into();
480 let v = value_to_json_string(&Value::Model(model)).unwrap();
481 assert_eq!(&v, "[true,false]");
482
483 let v = value_to_json_string(&Value::Struct(crate::Struct::from_iter([
484 ("kind".to_string(), Value::EnumerationValue("test".to_string(), "foo".to_string())),
485 ("is_bool".to_string(), Value::Bool(false)),
486 ("string-value".to_string(), Value::String("some string".into())),
487 ])))
488 .unwrap();
489 assert_eq!(&v, "{\"is-bool\":false,\"kind\":\"test.foo\",\"string-value\":\"some string\"}");
490
491 let v = value_to_json_string(&Value::Brush(Brush::SolidColor(Color::from_argb_u8(
492 0xff, 0x0a, 0xb0, 0xcd,
493 ))))
494 .unwrap();
495 assert_eq!(v, "\"#0ab0cdff\"".to_string());
496
497 let v = value_to_json_string(&Value::Brush(Brush::LinearGradient(
498 i_slint_core::graphics::LinearGradientBrush::new(
499 42.0,
500 vec![
501 i_slint_core::graphics::GradientStop {
502 position: 0.0,
503 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
504 },
505 i_slint_core::graphics::GradientStop {
506 position: 0.5,
507 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
508 },
509 i_slint_core::graphics::GradientStop {
510 position: 1.0,
511 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
512 },
513 ]
514 .drain(..),
515 ),
516 )))
517 .unwrap();
518 assert_eq!(&v, "\"@linear-gradient(42deg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"");
519
520 let v = value_to_json_string(&Value::Brush(Brush::RadialGradient(
521 i_slint_core::graphics::RadialGradientBrush::new_circle(
522 vec![
523 i_slint_core::graphics::GradientStop {
524 position: 0.0,
525 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
526 },
527 i_slint_core::graphics::GradientStop {
528 position: 0.5,
529 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
530 },
531 i_slint_core::graphics::GradientStop {
532 position: 1.0,
533 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
534 },
535 ]
536 .drain(..),
537 ),
538 )))
539 .unwrap();
540 assert_eq!(&v, "\"@radial-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"");
541}