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