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 if a == 255 {
216 format!("#{r:02x}{g:02x}{b:02x}")
217 } else {
218 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
219 }
220 }
221
222 fn gradient_to_string_helper<'a>(
223 prefix: String,
224 stops: impl Iterator<Item = &'a i_slint_core::graphics::GradientStop>,
225 ) -> serde_json::Value {
226 let mut gradient = prefix;
227
228 for stop in stops {
229 gradient += &format!(", {} {}%", color_to_string(&stop.color), stop.position * 100.0);
230 }
231
232 gradient += ")";
233
234 serde_json::Value::String(gradient)
235 }
236
237 match value {
238 Value::Void => Ok(serde_json::Value::Null),
239 Value::Bool(b) => Ok((*b).into()),
240 Value::Number(n) => {
241 let r = if *n == n.round() {
242 if *n >= 0.0 {
243 serde_json::Number::from_u128(*n as u128)
244 } else {
245 serde_json::Number::from_i128(*n as i128)
246 }
247 } else {
248 serde_json::Number::from_f64(*n)
249 };
250 if let Some(r) = r {
251 Ok(serde_json::Value::Number(r))
252 } else {
253 Err(format!("Could not convert {n} into a number"))
254 }
255 }
256 Value::EnumerationValue(e, v) => Ok(serde_json::Value::String(format!("{e}.{v}"))),
257 Value::String(shared_string) => Ok(serde_json::Value::String(shared_string.to_string())),
258 Value::Image(image) => {
259 if let Some(p) = image.path() {
260 Ok(serde_json::Value::String(format!("{}", p.to_string_lossy())))
261 } else {
262 Err("Cannot serialize an image without a path".into())
263 }
264 }
265 Value::Model(model_rc) => Ok(serde_json::Value::Array(
266 model_rc.iter().map(|v| v.to_json()).collect::<Result<Vec<_>, _>>()?,
267 )),
268 Value::Struct(s) => Ok(serde_json::Value::Object(
269 s.iter()
270 .map(|(k, v)| v.to_json().map(|v| (k.to_string(), v)))
271 .collect::<Result<serde_json::Map<_, _>, _>>()?,
272 )),
273 Value::Brush(brush) => match brush {
274 Brush::SolidColor(color) => Ok(serde_json::Value::String(color_to_string(color))),
275 Brush::LinearGradient(lg) => Ok(gradient_to_string_helper(
276 format!("@linear-gradient({}deg", lg.angle()),
277 lg.stops(),
278 )),
279 Brush::RadialGradient(rg) => {
280 Ok(gradient_to_string_helper("@radial-gradient(circle".into(), rg.stops()))
281 }
282 _ => Err("Cannot serialize an unknown brush type".into()),
283 },
284 Value::PathData(_) => Err("Cannot serialize path data".into()),
285 Value::EasingCurve(_) => Err("Cannot serialize a easing curve".into()),
286 _ => Err("Cannot serialize an unknown value type".into()),
287 }
288}
289
290pub fn value_to_json_string(value: &Value) -> Result<String, String> {
292 Ok(value_to_json(value)?.to_string())
293}
294
295#[test]
296fn test_from_json() {
297 let v = value_from_json_str(&langtype::Type::Void, "null").unwrap();
298 assert_eq!(v, Value::Void);
299 let v = Value::from_json_str(&langtype::Type::Void, "null").unwrap();
300 assert_eq!(v, Value::Void);
301
302 let v = value_from_json_str(&langtype::Type::Float32, "42.0").unwrap();
303 assert_eq!(v, Value::Number(42.0));
304
305 let v = value_from_json_str(&langtype::Type::Int32, "23").unwrap();
306 assert_eq!(v, Value::Number(23.0));
307
308 let v = value_from_json_str(&langtype::Type::String, "\"a string with \\\\ escape\"").unwrap();
309 assert_eq!(v, Value::String("a string with \\ escape".into()));
310
311 let v = value_from_json_str(&langtype::Type::Color, "\"#0ab0cdff\"").unwrap();
312 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
313 let v = value_from_json_str(&langtype::Type::Brush, "\"#0ab0cdff\"").unwrap();
314 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
315 assert_eq!(v, Value::Brush(Brush::SolidColor(Color::from_argb_u8(0xff, 0x0a, 0xb0, 0xcd))));
316 let v = value_from_json_str(
317 &langtype::Type::Brush,
318 "\"@linear-gradient(42deg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
319 )
320 .unwrap();
321 assert_eq!(
322 v,
323 Value::Brush(Brush::LinearGradient(i_slint_core::graphics::LinearGradientBrush::new(
324 42.0,
325 vec![
326 i_slint_core::graphics::GradientStop {
327 position: 0.0,
328 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
329 },
330 i_slint_core::graphics::GradientStop {
331 position: 0.5,
332 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
333 },
334 i_slint_core::graphics::GradientStop {
335 position: 1.0,
336 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
337 }
338 ]
339 .drain(..)
340 )))
341 );
342 assert!(value_from_json_str(
343 &langtype::Type::Brush,
344 "\"@linear-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
345 )
346 .is_err());
347 assert!(value_from_json_str(
348 &langtype::Type::Brush,
349 "\"@linear-gradient(#ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
350 )
351 .is_err());
352 assert!(value_from_json_str(
353 &langtype::Type::Brush,
354 "\"@linear-gradient(90turns, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
355 )
356 .is_err());
357 assert!(value_from_json_str(
358 &langtype::Type::Brush,
359 "\"@linear-gradient(xfdeg, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
360 )
361 .is_err());
362 assert!(value_from_json_str(
363 &langtype::Type::Brush,
364 "\"@linear-gradient(90deg, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
365 )
366 .is_err());
367 assert!(value_from_json_str(
368 &langtype::Type::Brush,
369 "\"@linear-gradient(90deg, #ff0000ff 0, #00ff00ff 50%, #0000ffff 100%)\""
370 )
371 .is_err());
372
373 let v = value_from_json_str(
374 &langtype::Type::Brush,
375 "\"@radial-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\"",
376 )
377 .unwrap();
378 assert_eq!(
379 v,
380 Value::Brush(Brush::RadialGradient(
381 i_slint_core::graphics::RadialGradientBrush::new_circle(
382 vec![
383 i_slint_core::graphics::GradientStop {
384 position: 0.0,
385 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00)
386 },
387 i_slint_core::graphics::GradientStop {
388 position: 0.5,
389 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00)
390 },
391 i_slint_core::graphics::GradientStop {
392 position: 1.0,
393 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff)
394 }
395 ]
396 .drain(..)
397 )
398 ))
399 );
400 assert!(value_from_json_str(
401 &langtype::Type::Brush,
402 "\"@radial-gradient(foobar, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
403 )
404 .is_err());
405 assert!(value_from_json_str(
406 &langtype::Type::Brush,
407 "\"@radial-gradient(circle, #xf0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
408 )
409 .is_err());
410 assert!(value_from_json_str(
411 &langtype::Type::Brush,
412 "\"@radial-gradient(circle, #ff0000ff 1000px, #00ff00ff 50%, #0000ffff 100%)\""
413 )
414 .is_err());
415 assert!(value_from_json_str(
416 &langtype::Type::Brush,
417 "\"@radial-gradient(circle, #ff0000ff 0% #00ff00ff 50%, #0000ffff 100%)\""
418 )
419 .is_err());
420 assert!(value_from_json_str(
421 &langtype::Type::Brush,
422 "\"@radial-gradient(circle, #ff0000ff, #0000ffff)\""
423 )
424 .is_err());
425
426 assert!(value_from_json_str(
427 &langtype::Type::Brush,
428 "\"@radial-gradient(conical, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
429 )
430 .is_err());
431
432 assert!(value_from_json_str(
433 &langtype::Type::Brush,
434 "\"@other-gradient(circle, #ff0000ff 0%, #00ff00ff 50%, #0000ffff 100%)\""
435 )
436 .is_err());
437}
438
439#[test]
440fn test_to_json() {
441 let v = value_to_json_string(&Value::Void).unwrap();
442 assert_eq!(&v, "null");
443 let v = Value::Void.to_json_string().unwrap();
444 assert_eq!(&v, "null");
445
446 let v = value_to_json_string(&Value::Number(23.0)).unwrap();
447 assert_eq!(&v, "23");
448
449 let v = value_to_json_string(&Value::Number(4.2)).unwrap();
450 assert_eq!(&v, "4.2");
451
452 let v = value_to_json_string(&Value::EnumerationValue("Foo".to_string(), "bar".to_string()))
453 .unwrap();
454 assert_eq!(&v, "\"Foo.bar\"");
455
456 let v = value_to_json_string(&Value::String("Hello World with \\ escaped".into())).unwrap();
457 assert_eq!(&v, "\"Hello World with \\\\ escaped\"");
458
459 let buffer = i_slint_core::graphics::SharedPixelBuffer::new(2, 2);
461 assert!(value_to_json_string(&Value::Image(Image::from_rgb8(buffer))).is_err());
462
463 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
465 .join("../../logo/MadeWithSlint-logo-dark.png")
466 .canonicalize()
467 .unwrap();
468 let v = value_to_json_string(&Value::Image(Image::load_from_path(&path).unwrap())).unwrap();
469 let path = path.to_string_lossy().replace("\\", "\\\\");
471 assert_eq!(v, format!("\"{path}\""));
472
473 let v = value_to_json_string(&Value::Bool(true)).unwrap();
474 assert_eq!(&v, "true");
475
476 let v = value_to_json_string(&Value::Bool(false)).unwrap();
477 assert_eq!(&v, "false");
478
479 let model: ModelRc<Value> = std::rc::Rc::new(i_slint_core::model::VecModel::from(vec![
480 Value::Bool(true),
481 Value::Bool(false),
482 ]))
483 .into();
484 let v = value_to_json_string(&Value::Model(model)).unwrap();
485 assert_eq!(&v, "[true,false]");
486
487 let v = value_to_json_string(&Value::Struct(crate::Struct::from_iter([
488 ("kind".to_string(), Value::EnumerationValue("test".to_string(), "foo".to_string())),
489 ("is_bool".to_string(), Value::Bool(false)),
490 ("string-value".to_string(), Value::String("some string".into())),
491 ])))
492 .unwrap();
493 assert_eq!(&v, "{\"is-bool\":false,\"kind\":\"test.foo\",\"string-value\":\"some string\"}");
494
495 let v = value_to_json_string(&Value::Brush(Brush::SolidColor(Color::from_argb_u8(
496 0xff, 0x0a, 0xb0, 0xcd,
497 ))))
498 .unwrap();
499 assert_eq!(v, "\"#0ab0cd\"".to_string());
500
501 let v = value_to_json_string(&Value::Brush(Brush::LinearGradient(
502 i_slint_core::graphics::LinearGradientBrush::new(
503 42.0,
504 vec![
505 i_slint_core::graphics::GradientStop {
506 position: 0.0,
507 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
508 },
509 i_slint_core::graphics::GradientStop {
510 position: 0.5,
511 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
512 },
513 i_slint_core::graphics::GradientStop {
514 position: 1.0,
515 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
516 },
517 ]
518 .drain(..),
519 ),
520 )))
521 .unwrap();
522 assert_eq!(&v, "\"@linear-gradient(42deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
523
524 let v = value_to_json_string(&Value::Brush(Brush::RadialGradient(
525 i_slint_core::graphics::RadialGradientBrush::new_circle(
526 vec![
527 i_slint_core::graphics::GradientStop {
528 position: 0.0,
529 color: Color::from_argb_u8(0xff, 0xff, 0x00, 0x00),
530 },
531 i_slint_core::graphics::GradientStop {
532 position: 0.5,
533 color: Color::from_argb_u8(0xff, 0x00, 0xff, 0x00),
534 },
535 i_slint_core::graphics::GradientStop {
536 position: 1.0,
537 color: Color::from_argb_u8(0xff, 0x00, 0x00, 0xff),
538 },
539 ]
540 .drain(..),
541 ),
542 )))
543 .unwrap();
544 assert_eq!(&v, "\"@radial-gradient(circle, #ff0000 0%, #00ff00 50%, #0000ff 100%)\"");
545}