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#[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 Value::String(s) => Ok(Self::Path(s.to_string())),
640 Value::Bytes(b) => Ok(Self::Bytes((*b).clone())),
642 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#[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#[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}