Skip to main content

graphix_package_gui/
types.rs

1use crate::theme::{
2    ButtonSpec, CheckboxSpec, ContainerSpec, GraphixTheme, MenuSpec, PickListSpec,
3    ProgressBarSpec, RadioSpec, RuleSpec, ScrollableSpec, SliderSpec, StyleOverrides,
4    TextEditorSpec, TextInputSpec, TogglerSpec,
5};
6use anyhow::{bail, Context, Result};
7use arcstr::ArcStr;
8use iced_core::{
9    alignment::{Horizontal, Vertical},
10    font::{Family, Style, Weight},
11    Color, ContentFit, Font, Length, Padding, Size,
12};
13use iced_widget::{scrollable, tooltip};
14use netidx::publisher::{FromValue, Value};
15use smallvec::SmallVec;
16use std::{
17    collections::HashSet,
18    sync::{LazyLock, Mutex},
19};
20use triomphe::Arc;
21
22static FONT_NAMES: LazyLock<Mutex<HashSet<&'static str>>> =
23    LazyLock::new(Default::default);
24
25#[derive(Clone, Copy, Debug)]
26pub struct LengthV(pub Length);
27
28impl FromValue for LengthV {
29    fn from_value(v: Value) -> Result<Self> {
30        match v {
31            Value::String(s) => match &*s {
32                "Fill" => Ok(Self(Length::Fill)),
33                "Shrink" => Ok(Self(Length::Shrink)),
34                s => bail!("invalid length {s}"),
35            },
36            v => match v.cast_to::<(ArcStr, Value)>()? {
37                (s, v) if &*s == "FillPortion" => {
38                    let n = v.cast_to::<u16>()?;
39                    Ok(Self(Length::FillPortion(n)))
40                }
41                (s, v) if &*s == "Fixed" => {
42                    let n = v.cast_to::<f64>()? as f32;
43                    Ok(Self(Length::Fixed(n)))
44                }
45                (s, _) => bail!("invalid length {s}"),
46            },
47        }
48    }
49}
50
51#[derive(Clone, Copy, Debug)]
52pub struct PaddingV(pub Padding);
53
54impl FromValue for PaddingV {
55    fn from_value(v: Value) -> Result<Self> {
56        match v.cast_to::<(ArcStr, Value)>()? {
57            (s, v) if &*s == "All" => {
58                let n = v.cast_to::<f64>()? as f32;
59                Ok(Self(Padding::new(n)))
60            }
61            (s, v) if &*s == "Axis" => {
62                let [(_, x), (_, y)] = v.cast_to::<[(ArcStr, f64); 2]>()?;
63                Ok(Self(Padding::from([y as f32, x as f32])))
64            }
65            (s, v) if &*s == "Each" => {
66                let [(_, bottom), (_, left), (_, right), (_, top)] =
67                    v.cast_to::<[(ArcStr, f64); 4]>()?;
68                Ok(Self(Padding {
69                    top: top as f32,
70                    right: right as f32,
71                    bottom: bottom as f32,
72                    left: left as f32,
73                }))
74            }
75            (s, _) => bail!("invalid padding {s}"),
76        }
77    }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct SizeV(pub Size);
82
83impl FromValue for SizeV {
84    fn from_value(v: Value) -> Result<Self> {
85        let [(_, height), (_, width)] = v.cast_to::<[(ArcStr, f64); 2]>()?;
86        Ok(Self(Size::new(width as f32, height as f32)))
87    }
88}
89
90impl From<SizeV> for Value {
91    fn from(v: SizeV) -> Value {
92        use arcstr::literal;
93        [(literal!("height"), v.0.height as f64), (literal!("width"), v.0.width as f64)]
94            .into()
95    }
96}
97
98#[derive(Clone, Copy, Debug)]
99pub struct ColorV(pub Color);
100
101impl FromValue for ColorV {
102    fn from_value(v: Value) -> Result<Self> {
103        let [(_, a), (_, b), (_, g), (_, r)] = v.cast_to::<[(ArcStr, f64); 4]>()?;
104        let [r, g, b, a] = [r as f32, g as f32, b as f32, a as f32];
105        if !(0.0..=1.0).contains(&r)
106            || !(0.0..=1.0).contains(&g)
107            || !(0.0..=1.0).contains(&b)
108            || !(0.0..=1.0).contains(&a)
109        {
110            bail!("color components must be in [0, 1], got r={r} g={g} b={b} a={a}");
111        }
112        Ok(Self(Color::from_rgba(r, g, b, a)))
113    }
114}
115
116#[derive(Clone, Copy, Debug)]
117pub struct HAlignV(pub Horizontal);
118
119impl FromValue for HAlignV {
120    fn from_value(v: Value) -> Result<Self> {
121        match &*v.cast_to::<ArcStr>()? {
122            "Left" => Ok(Self(Horizontal::Left)),
123            "Center" => Ok(Self(Horizontal::Center)),
124            "Right" => Ok(Self(Horizontal::Right)),
125            s => bail!("invalid halign {s}"),
126        }
127    }
128}
129
130#[derive(Clone, Copy, Debug)]
131pub struct VAlignV(pub Vertical);
132
133impl FromValue for VAlignV {
134    fn from_value(v: Value) -> Result<Self> {
135        match &*v.cast_to::<ArcStr>()? {
136            "Top" => Ok(Self(Vertical::Top)),
137            "Center" => Ok(Self(Vertical::Center)),
138            "Bottom" => Ok(Self(Vertical::Bottom)),
139            s => bail!("invalid valign {s}"),
140        }
141    }
142}
143
144#[derive(Clone, Copy, Debug)]
145pub struct FontV(pub Font);
146
147impl FromValue for FontV {
148    fn from_value(v: Value) -> Result<Self> {
149        let [(_, family), (_, style), (_, weight)] =
150            v.cast_to::<[(ArcStr, Value); 3]>()?;
151        let family = match family {
152            Value::String(s) => match &*s {
153                "SansSerif" => Family::SansSerif,
154                "Serif" => Family::Serif,
155                "Monospace" => Family::Monospace,
156                s => bail!("invalid font family {s}"),
157            },
158            v => match v.cast_to::<(ArcStr, Value)>()? {
159                (s, v) if &*s == "Name" => {
160                    let name = v.cast_to::<ArcStr>()?;
161                    let mut cache = FONT_NAMES.lock().unwrap();
162                    let interned = match cache.get(name.as_str()) {
163                        Some(&s) => s,
164                        None => {
165                            let leaked: &'static str =
166                                Box::leak(name.to_string().into_boxed_str());
167                            cache.insert(leaked);
168                            leaked
169                        }
170                    };
171                    Family::Name(interned)
172                }
173                (s, _) => bail!("invalid font family {s}"),
174            },
175        };
176        let weight = match &*weight.cast_to::<ArcStr>()? {
177            "Thin" => Weight::Thin,
178            "ExtraLight" => Weight::ExtraLight,
179            "Light" => Weight::Light,
180            "Normal" => Weight::Normal,
181            "Medium" => Weight::Medium,
182            "SemiBold" => Weight::Semibold,
183            "Bold" => Weight::Bold,
184            "ExtraBold" => Weight::ExtraBold,
185            "Black" => Weight::Black,
186            s => bail!("invalid font weight {s}"),
187        };
188        let style = match &*style.cast_to::<ArcStr>()? {
189            "Normal" => Style::Normal,
190            "Italic" => Style::Italic,
191            "Oblique" => Style::Oblique,
192            s => bail!("invalid font style {s}"),
193        };
194        Ok(Self(Font { family, weight, style, ..Font::DEFAULT }))
195    }
196}
197
198#[derive(Clone, Copy, Debug)]
199pub struct PaletteV(pub iced_core::theme::palette::Palette);
200
201impl FromValue for PaletteV {
202    fn from_value(v: Value) -> Result<Self> {
203        let [(_, bg), (_, danger), (_, primary), (_, success), (_, text), (_, warning)] =
204            v.cast_to::<[(ArcStr, Value); 6]>()?;
205        let bg = ColorV::from_value(bg)?;
206        let text = ColorV::from_value(text)?;
207        let primary = ColorV::from_value(primary)?;
208        let success = ColorV::from_value(success)?;
209        let warning = ColorV::from_value(warning)?;
210        let danger = ColorV::from_value(danger)?;
211        Ok(Self(iced_core::theme::palette::Palette {
212            background: bg.0,
213            text: text.0,
214            primary: primary.0,
215            success: success.0,
216            warning: warning.0,
217            danger: danger.0,
218        }))
219    }
220}
221
222#[derive(Clone, Debug)]
223pub struct ThemeV(pub GraphixTheme);
224
225pub fn parse_opt_color(v: Value) -> Result<Option<Color>> {
226    if v == Value::Null {
227        Ok(None)
228    } else {
229        Ok(Some(ColorV::from_value(v)?.0))
230    }
231}
232
233fn parse_opt_f32(v: Value) -> Result<Option<f32>> {
234    if v == Value::Null {
235        Ok(None)
236    } else {
237        Ok(Some(v.cast_to::<f64>()? as f32))
238    }
239}
240
241fn parse_opt_spec<T>(v: Value, f: impl FnOnce(Value) -> Result<T>) -> Result<Option<T>> {
242    if v == Value::Null {
243        Ok(None)
244    } else {
245        Ok(Some(f(v)?))
246    }
247}
248
249fn parse_button_spec(v: Value) -> Result<ButtonSpec> {
250    let [(_, bg), (_, bc), (_, br), (_, bw), (_, tc)] =
251        v.cast_to::<[(ArcStr, Value); 5]>()?;
252    Ok(ButtonSpec {
253        background: parse_opt_color(bg)?,
254        border_color: parse_opt_color(bc)?,
255        border_radius: parse_opt_f32(br)?,
256        border_width: parse_opt_f32(bw)?,
257        text_color: parse_opt_color(tc)?,
258    })
259}
260
261fn parse_checkbox_spec(v: Value) -> Result<CheckboxSpec> {
262    let [(_, accent), (_, bg), (_, bc), (_, br), (_, bw), (_, ic), (_, tc)] =
263        v.cast_to::<[(ArcStr, Value); 7]>()?;
264    Ok(CheckboxSpec {
265        accent: parse_opt_color(accent)?,
266        background: parse_opt_color(bg)?,
267        border_color: parse_opt_color(bc)?,
268        border_radius: parse_opt_f32(br)?,
269        border_width: parse_opt_f32(bw)?,
270        icon_color: parse_opt_color(ic)?,
271        text_color: parse_opt_color(tc)?,
272    })
273}
274
275fn parse_container_spec(v: Value) -> Result<ContainerSpec> {
276    let [(_, bg), (_, bc), (_, br), (_, bw), (_, tc)] =
277        v.cast_to::<[(ArcStr, Value); 5]>()?;
278    Ok(ContainerSpec {
279        background: parse_opt_color(bg)?,
280        border_color: parse_opt_color(bc)?,
281        border_radius: parse_opt_f32(br)?,
282        border_width: parse_opt_f32(bw)?,
283        text_color: parse_opt_color(tc)?,
284    })
285}
286
287fn parse_menu_spec(v: Value) -> Result<MenuSpec> {
288    let [(_, bg), (_, bc), (_, br), (_, bw), (_, sb), (_, stc), (_, tc)] =
289        v.cast_to::<[(ArcStr, Value); 7]>()?;
290    Ok(MenuSpec {
291        background: parse_opt_color(bg)?,
292        border_color: parse_opt_color(bc)?,
293        border_radius: parse_opt_f32(br)?,
294        border_width: parse_opt_f32(bw)?,
295        selected_background: parse_opt_color(sb)?,
296        selected_text_color: parse_opt_color(stc)?,
297        text_color: parse_opt_color(tc)?,
298    })
299}
300
301fn parse_pick_list_spec(v: Value) -> Result<PickListSpec> {
302    let [(_, bg), (_, bc), (_, br), (_, bw), (_, hc), (_, pc), (_, tc)] =
303        v.cast_to::<[(ArcStr, Value); 7]>()?;
304    Ok(PickListSpec {
305        background: parse_opt_color(bg)?,
306        border_color: parse_opt_color(bc)?,
307        border_radius: parse_opt_f32(br)?,
308        border_width: parse_opt_f32(bw)?,
309        handle_color: parse_opt_color(hc)?,
310        placeholder_color: parse_opt_color(pc)?,
311        text_color: parse_opt_color(tc)?,
312    })
313}
314
315fn parse_progress_bar_spec(v: Value) -> Result<ProgressBarSpec> {
316    let [(_, bg), (_, bar), (_, br)] = v.cast_to::<[(ArcStr, Value); 3]>()?;
317    Ok(ProgressBarSpec {
318        background: parse_opt_color(bg)?,
319        bar_color: parse_opt_color(bar)?,
320        border_radius: parse_opt_f32(br)?,
321    })
322}
323
324fn parse_radio_spec(v: Value) -> Result<RadioSpec> {
325    let [(_, bg), (_, bc), (_, bw), (_, dc), (_, tc)] =
326        v.cast_to::<[(ArcStr, Value); 5]>()?;
327    Ok(RadioSpec {
328        background: parse_opt_color(bg)?,
329        border_color: parse_opt_color(bc)?,
330        border_width: parse_opt_f32(bw)?,
331        dot_color: parse_opt_color(dc)?,
332        text_color: parse_opt_color(tc)?,
333    })
334}
335
336fn parse_rule_spec(v: Value) -> Result<RuleSpec> {
337    let [(_, color), (_, radius), (_, width)] = v.cast_to::<[(ArcStr, Value); 3]>()?;
338    Ok(RuleSpec {
339        color: parse_opt_color(color)?,
340        radius: parse_opt_f32(radius)?,
341        width: parse_opt_f32(width)?,
342    })
343}
344
345fn parse_scrollable_spec(v: Value) -> Result<ScrollableSpec> {
346    let [(_, bg), (_, bc), (_, br), (_, bw), (_, sc)] =
347        v.cast_to::<[(ArcStr, Value); 5]>()?;
348    Ok(ScrollableSpec {
349        background: parse_opt_color(bg)?,
350        border_color: parse_opt_color(bc)?,
351        border_radius: parse_opt_f32(br)?,
352        border_width: parse_opt_f32(bw)?,
353        scroller_color: parse_opt_color(sc)?,
354    })
355}
356
357fn parse_slider_spec(v: Value) -> Result<SliderSpec> {
358    let [(_, hbc), (_, hbw), (_, hc), (_, hr), (_, rc), (_, rfc), (_, rw)] =
359        v.cast_to::<[(ArcStr, Value); 7]>()?;
360    Ok(SliderSpec {
361        handle_border_color: parse_opt_color(hbc)?,
362        handle_border_width: parse_opt_f32(hbw)?,
363        handle_color: parse_opt_color(hc)?,
364        handle_radius: parse_opt_f32(hr)?,
365        rail_color: parse_opt_color(rc)?,
366        rail_fill_color: parse_opt_color(rfc)?,
367        rail_width: parse_opt_f32(rw)?,
368    })
369}
370
371fn parse_text_editor_spec(v: Value) -> Result<TextEditorSpec> {
372    let [(_, bg), (_, bc), (_, br), (_, bw), (_, pc), (_, sc), (_, vc)] =
373        v.cast_to::<[(ArcStr, Value); 7]>()?;
374    Ok(TextEditorSpec {
375        background: parse_opt_color(bg)?,
376        border_color: parse_opt_color(bc)?,
377        border_radius: parse_opt_f32(br)?,
378        border_width: parse_opt_f32(bw)?,
379        placeholder_color: parse_opt_color(pc)?,
380        selection_color: parse_opt_color(sc)?,
381        value_color: parse_opt_color(vc)?,
382    })
383}
384
385fn parse_text_input_spec(v: Value) -> Result<TextInputSpec> {
386    let [(_, bg), (_, bc), (_, br), (_, bw), (_, ic), (_, pc), (_, sc), (_, vc)] =
387        v.cast_to::<[(ArcStr, Value); 8]>()?;
388    Ok(TextInputSpec {
389        background: parse_opt_color(bg)?,
390        border_color: parse_opt_color(bc)?,
391        border_radius: parse_opt_f32(br)?,
392        border_width: parse_opt_f32(bw)?,
393        icon_color: parse_opt_color(ic)?,
394        placeholder_color: parse_opt_color(pc)?,
395        selection_color: parse_opt_color(sc)?,
396        value_color: parse_opt_color(vc)?,
397    })
398}
399
400fn parse_toggler_spec(v: Value) -> Result<TogglerSpec> {
401    let [(_, bg), (_, bbc), (_, br), (_, fg), (_, fbc), (_, tc)] =
402        v.cast_to::<[(ArcStr, Value); 6]>()?;
403    Ok(TogglerSpec {
404        background: parse_opt_color(bg)?,
405        background_border_color: parse_opt_color(bbc)?,
406        border_radius: parse_opt_f32(br)?,
407        foreground: parse_opt_color(fg)?,
408        foreground_border_color: parse_opt_color(fbc)?,
409        text_color: parse_opt_color(tc)?,
410    })
411}
412
413fn parse_stylesheet(
414    v: Value,
415) -> Result<(iced_core::theme::palette::Palette, StyleOverrides)> {
416    let [(_, button), (_, checkbox), (_, container), (_, menu), (_, palette), (_, pick_list), (_, progress_bar), (_, radio), (_, rule), (_, scrollable), (_, slider), (_, text_editor), (_, text_input), (_, toggler)] =
417        v.cast_to::<[(ArcStr, Value); 14]>()?;
418    let palette = PaletteV::from_value(palette)?;
419    Ok((
420        palette.0,
421        StyleOverrides {
422            button: parse_opt_spec(button, parse_button_spec)?,
423            checkbox: parse_opt_spec(checkbox, parse_checkbox_spec)?,
424            container: parse_opt_spec(container, parse_container_spec)?,
425            menu: parse_opt_spec(menu, parse_menu_spec)?,
426            pick_list: parse_opt_spec(pick_list, parse_pick_list_spec)?,
427            progress_bar: parse_opt_spec(progress_bar, parse_progress_bar_spec)?,
428            radio: parse_opt_spec(radio, parse_radio_spec)?,
429            rule: parse_opt_spec(rule, parse_rule_spec)?,
430            scrollable: parse_opt_spec(scrollable, parse_scrollable_spec)?,
431            slider: parse_opt_spec(slider, parse_slider_spec)?,
432            text_editor: parse_opt_spec(text_editor, parse_text_editor_spec)?,
433            text_input: parse_opt_spec(text_input, parse_text_input_spec)?,
434            toggler: parse_opt_spec(toggler, parse_toggler_spec)?,
435        },
436    ))
437}
438
439impl FromValue for ThemeV {
440    fn from_value(v: Value) -> Result<Self> {
441        use iced_core::Theme;
442        match v {
443            Value::String(s) => {
444                let inner = match &*s {
445                    "Light" => Theme::Light,
446                    "Dark" => Theme::Dark,
447                    "Dracula" => Theme::Dracula,
448                    "Nord" => Theme::Nord,
449                    "SolarizedLight" => Theme::SolarizedLight,
450                    "SolarizedDark" => Theme::SolarizedDark,
451                    "GruvboxLight" => Theme::GruvboxLight,
452                    "GruvboxDark" => Theme::GruvboxDark,
453                    "CatppuccinLatte" => Theme::CatppuccinLatte,
454                    "CatppuccinFrappe" => Theme::CatppuccinFrappe,
455                    "CatppuccinMacchiato" => Theme::CatppuccinMacchiato,
456                    "CatppuccinMocha" => Theme::CatppuccinMocha,
457                    "TokyoNight" => Theme::TokyoNight,
458                    "TokyoNightStorm" => Theme::TokyoNightStorm,
459                    "TokyoNightLight" => Theme::TokyoNightLight,
460                    "KanagawaWave" => Theme::KanagawaWave,
461                    "KanagawaDragon" => Theme::KanagawaDragon,
462                    "KanagawaLotus" => Theme::KanagawaLotus,
463                    "Moonfly" => Theme::Moonfly,
464                    "Nightfly" => Theme::Nightfly,
465                    "Oxocarbon" => Theme::Oxocarbon,
466                    "Ferra" => Theme::Ferra,
467                    s => bail!("invalid theme {s}"),
468                };
469                Ok(Self(GraphixTheme { inner, overrides: None }))
470            }
471            v => match v.cast_to::<(ArcStr, Value)>()? {
472                (s, v) if &*s == "CustomPalette" => {
473                    let palette = PaletteV::from_value(v)?;
474                    Ok(Self(GraphixTheme {
475                        inner: Theme::custom("Custom", palette.0),
476                        overrides: None,
477                    }))
478                }
479                (s, v) if &*s == "Custom" => {
480                    let (palette, overrides) = parse_stylesheet(v)?;
481                    Ok(Self(GraphixTheme {
482                        inner: Theme::custom("Custom", palette),
483                        overrides: Some(Arc::new(overrides)),
484                    }))
485                }
486                (s, _) => bail!("invalid theme {s}"),
487            },
488        }
489    }
490}
491
492#[derive(Clone, Copy, Debug)]
493pub struct ScrollDirectionV(pub scrollable::Direction);
494
495impl FromValue for ScrollDirectionV {
496    fn from_value(v: Value) -> Result<Self> {
497        match &*v.cast_to::<ArcStr>()? {
498            "Vertical" => Ok(Self(scrollable::Direction::Vertical(
499                scrollable::Scrollbar::default(),
500            ))),
501            "Horizontal" => Ok(Self(scrollable::Direction::Horizontal(
502                scrollable::Scrollbar::default(),
503            ))),
504            "Both" => Ok(Self(scrollable::Direction::Both {
505                vertical: scrollable::Scrollbar::default(),
506                horizontal: scrollable::Scrollbar::default(),
507            })),
508            s => bail!("invalid scroll direction {s}"),
509        }
510    }
511}
512
513#[derive(Clone, Copy, Debug)]
514pub struct TooltipPositionV(pub tooltip::Position);
515
516impl FromValue for TooltipPositionV {
517    fn from_value(v: Value) -> Result<Self> {
518        match &*v.cast_to::<ArcStr>()? {
519            "Top" => Ok(Self(tooltip::Position::Top)),
520            "Bottom" => Ok(Self(tooltip::Position::Bottom)),
521            "Left" => Ok(Self(tooltip::Position::Left)),
522            "Right" => Ok(Self(tooltip::Position::Right)),
523            "FollowCursor" => Ok(Self(tooltip::Position::FollowCursor)),
524            s => bail!("invalid tooltip position {s}"),
525        }
526    }
527}
528
529#[derive(Clone, Copy, Debug)]
530pub struct ContentFitV(pub ContentFit);
531
532impl FromValue for ContentFitV {
533    fn from_value(v: Value) -> Result<Self> {
534        match &*v.cast_to::<ArcStr>()? {
535            "Fill" => Ok(Self(ContentFit::Fill)),
536            "Contain" => Ok(Self(ContentFit::Contain)),
537            "Cover" => Ok(Self(ContentFit::Cover)),
538            "None" => Ok(Self(ContentFit::None)),
539            "ScaleDown" => Ok(Self(ContentFit::ScaleDown)),
540            s => bail!("invalid content fit {s}"),
541        }
542    }
543}
544
545/// Image source: file path, raw encoded bytes, inline SVG, or decoded RGBA pixels.
546#[derive(Clone, Debug)]
547pub enum ImageSourceV {
548    Path(String),
549    Bytes(iced_core::Bytes),
550    Svg(String),
551    Rgba { width: u32, height: u32, pixels: iced_core::Bytes },
552}
553
554impl ImageSourceV {
555    pub fn is_svg(&self) -> bool {
556        match self {
557            Self::Path(p) => p.ends_with(".svg") || p.ends_with(".svgz"),
558            Self::Svg(_) => true,
559            _ => false,
560        }
561    }
562
563    pub fn to_handle(&self) -> iced_core::image::Handle {
564        match self {
565            Self::Path(p) => iced_core::image::Handle::from_path(p),
566            Self::Bytes(b) => iced_core::image::Handle::from_bytes(b.clone()),
567            Self::Svg(_) => iced_core::image::Handle::from_path(""),
568            Self::Rgba { width, height, pixels } => {
569                iced_core::image::Handle::from_rgba(*width, *height, pixels.clone())
570            }
571        }
572    }
573
574    pub fn to_svg_handle(&self) -> iced_core::svg::Handle {
575        match self {
576            Self::Path(p) => iced_core::svg::Handle::from_path(p),
577            Self::Bytes(b) => iced_core::svg::Handle::from_memory(b.to_vec()),
578            Self::Svg(s) => iced_core::svg::Handle::from_memory(s.as_bytes().to_vec()),
579            Self::Rgba { .. } => iced_core::svg::Handle::from_path(""),
580        }
581    }
582
583    pub fn decode_icon(&self) -> Result<Option<winit::window::Icon>> {
584        match self {
585            Self::Path(p) if p.is_empty() => Ok(None),
586            Self::Path(p) if self.is_svg() => {
587                let data = std::fs::read(p)?;
588                decode_svg_icon(&data)
589            }
590            Self::Path(p) => {
591                let img = ::image::open(p)?.into_rgba8();
592                let (w, h) = img.dimensions();
593                Ok(Some(winit::window::Icon::from_rgba(img.into_raw(), w, h)?))
594            }
595            Self::Bytes(b) if b.is_empty() => Ok(None),
596            Self::Bytes(b) => {
597                let img = ::image::load_from_memory(b)?.into_rgba8();
598                let (w, h) = img.dimensions();
599                Ok(Some(winit::window::Icon::from_rgba(img.into_raw(), w, h)?))
600            }
601            Self::Svg(s) if s.is_empty() => Ok(None),
602            Self::Svg(s) => decode_svg_icon(s.as_bytes()),
603            Self::Rgba { width, height, pixels } => {
604                if pixels.is_empty() {
605                    return Ok(None);
606                }
607                Ok(Some(winit::window::Icon::from_rgba(
608                    pixels.to_vec(),
609                    *width,
610                    *height,
611                )?))
612            }
613        }
614    }
615}
616
617fn decode_svg_icon(data: &[u8]) -> Result<Option<winit::window::Icon>> {
618    let tree = resvg::usvg::Tree::from_data(data, &Default::default())?;
619    let size = 32;
620    let svg_size = tree.size();
621    let sx = size as f32 / svg_size.width();
622    let sy = size as f32 / svg_size.height();
623    let scale = sx.min(sy);
624    let mut pixmap = resvg::tiny_skia::Pixmap::new(size, size)
625        .context("failed to allocate pixmap for SVG icon")?;
626    let transform = resvg::tiny_skia::Transform::from_scale(scale, scale);
627    resvg::render(&tree, transform, &mut pixmap.as_mut());
628    Ok(Some(winit::window::Icon::from_rgba(
629        pixmap.data().to_vec(),
630        size,
631        size,
632    )?))
633}
634
635impl FromValue for ImageSourceV {
636    fn from_value(v: Value) -> Result<Self> {
637        match v {
638            // Bare string → file path
639            Value::String(s) => Ok(Self::Path(s.to_string())),
640            // Bare bytes → encoded image data
641            Value::Bytes(b) => Ok(Self::Bytes((*b).clone())),
642            // Variant tag
643            v => {
644                let (tag, val) = v.cast_to::<(ArcStr, Value)>()?;
645                match &*tag {
646                    "Bytes" => match val {
647                        Value::Bytes(b) => Ok(Self::Bytes((*b).clone())),
648                        _ => bail!("ImageSource Bytes: expected bytes value"),
649                    },
650                    "Svg" => Ok(Self::Svg(val.cast_to::<String>()?)),
651                    "Rgba" => {
652                        let [(_, height), (_, pixels), (_, width)] =
653                            val.cast_to::<[(ArcStr, Value); 3]>()?;
654                        let width = width.cast_to::<u32>()?;
655                        let height = height.cast_to::<u32>()?;
656                        let pixels = match pixels {
657                            Value::Bytes(b) => (*b).clone(),
658                            _ => bail!("ImageSource Rgba: expected bytes for pixels"),
659                        };
660                        Ok(Self::Rgba { width, height, pixels })
661                    }
662                    s => bail!("invalid ImageSource variant: {s}"),
663                }
664            }
665        }
666    }
667}
668
669#[derive(Clone, Copy, Debug)]
670pub enum GridColumnsV {
671    Fixed(usize),
672    Fluid(f32),
673}
674
675impl FromValue for GridColumnsV {
676    fn from_value(v: Value) -> Result<Self> {
677        match v {
678            v => match v.cast_to::<(ArcStr, Value)>()? {
679                (s, v) if &*s == "Fixed" => {
680                    let n = v.cast_to::<i64>()? as usize;
681                    Ok(Self::Fixed(n))
682                }
683                (s, v) if &*s == "Fluid" => {
684                    let n = v.cast_to::<f64>()? as f32;
685                    Ok(Self::Fluid(n))
686                }
687                (s, _) => bail!("invalid grid columns {s}"),
688            },
689        }
690    }
691}
692
693#[derive(Clone, Copy, Debug)]
694pub struct GridSizingV(pub iced_widget::grid::Sizing);
695
696impl FromValue for GridSizingV {
697    fn from_value(v: Value) -> Result<Self> {
698        match v.cast_to::<(ArcStr, Value)>()? {
699            (s, v) if &*s == "AspectRatio" => {
700                let r = v.cast_to::<f64>()? as f32;
701                Ok(Self(iced_widget::grid::Sizing::AspectRatio(r)))
702            }
703            (s, v) if &*s == "EvenlyDistribute" => {
704                let l = LengthV::from_value(v)?;
705                Ok(Self(iced_widget::grid::Sizing::EvenlyDistribute(l.0)))
706            }
707            (s, _) => bail!("invalid grid sizing {s}"),
708        }
709    }
710}
711
712/// Parsed shortcut from the Graphix `Shortcut` struct.
713#[derive(Clone, Debug)]
714pub struct ShortcutV {
715    pub display: String,
716    pub key: iced_core::keyboard::Key,
717    pub modifiers: iced_core::keyboard::Modifiers,
718}
719
720impl FromValue for ShortcutV {
721    fn from_value(v: Value) -> Result<Self> {
722        let [(_, alt), (_, ctrl), (_, key), (_, logo), (_, shift)] =
723            v.cast_to::<[(ArcStr, Value); 5]>()?;
724        let alt = alt.cast_to::<bool>()?;
725        let ctrl = ctrl.cast_to::<bool>()?;
726        let key_str = key.cast_to::<ArcStr>()?;
727        let logo = logo.cast_to::<bool>()?;
728        let shift = shift.cast_to::<bool>()?;
729        let mut display = String::new();
730        if ctrl {
731            display.push_str("Ctrl+");
732        }
733        if alt {
734            display.push_str("Alt+");
735        }
736        if shift {
737            display.push_str("Shift+");
738        }
739        if logo {
740            display.push_str("Super+");
741        }
742        display.push_str(&key_str.to_uppercase());
743        let mut modifiers = iced_core::keyboard::Modifiers::empty();
744        if ctrl {
745            modifiers |= iced_core::keyboard::Modifiers::CTRL;
746        }
747        if alt {
748            modifiers |= iced_core::keyboard::Modifiers::ALT;
749        }
750        if shift {
751            modifiers |= iced_core::keyboard::Modifiers::SHIFT;
752        }
753        if logo {
754            modifiers |= iced_core::keyboard::Modifiers::LOGO;
755        }
756        let iced_key =
757            iced_core::keyboard::Key::Character(key_str.to_lowercase().into());
758        Ok(Self { display, key: iced_key, modifiers })
759    }
760}
761
762/// Newtype for `Vec<String>` to satisfy orphan rules.
763#[derive(Clone, Debug)]
764pub struct StringVec(pub Vec<String>);
765
766impl FromValue for StringVec {
767    fn from_value(v: Value) -> Result<Self> {
768        let items = v.cast_to::<SmallVec<[Value; 8]>>()?;
769        let v: Vec<String> =
770            items.into_iter().map(|v| v.cast_to::<String>()).collect::<Result<_>>()?;
771        Ok(Self(v))
772    }
773}