Skip to main content

freya_components/
svg_viewer.rs

1use std::{
2    rc::Rc,
3    sync::LazyLock,
4};
5
6use anyhow::Context;
7use async_lock::Semaphore;
8use bytes::Bytes;
9use freya_core::{
10    element::EventHandlerType,
11    elements::image::*,
12    events::name::EventName,
13    prelude::*,
14};
15use freya_engine::prelude::{
16    FontMgr,
17    SkImage,
18    raster_n32_premul,
19    svg,
20};
21use rustc_hash::FxHashMap;
22use torin::prelude::{
23    Size,
24    Size2D,
25};
26
27#[cfg(feature = "remote-asset")]
28use crate::http::Http;
29use crate::{
30    cache::*,
31    image_viewer::{
32        DecodeSize,
33        ImageSource,
34    },
35    loader::CircularLoader,
36    theming::hooks::get_theme_or_default,
37};
38
39/// Limit the amount of SVGs rasterized in parallel.
40static RASTER_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
41
42/// Color and stroke overrides applied to an SVG before it is rasterized.
43#[derive(Default, Clone, Copy, PartialEq)]
44struct SvgStyle {
45    color: Option<Color>,
46    fill: Option<Color>,
47    stroke: Option<Color>,
48    stroke_width: Option<f32>,
49}
50
51impl SvgStyle {
52    /// A hashable representation, since `f32` is not [`Hash`].
53    fn as_key(&self) -> (Option<Color>, Option<Color>, Option<Color>, Option<u32>) {
54        (
55            self.color,
56            self.fill,
57            self.stroke,
58            self.stroke_width.map(f32::to_bits),
59        )
60    }
61}
62
63/// Fetch, parse and rasterize an SVG at `size` off the main thread.
64async fn rasterize(
65    source: ImageSource,
66    size: DecodeSize,
67    style: SvgStyle,
68) -> anyhow::Result<SkImage> {
69    #[cfg(feature = "remote-asset")]
70    let bytes = {
71        let client = Http::get();
72        blocking::unblock(move || source.fetch(&client)).await?
73    };
74    #[cfg(not(feature = "remote-asset"))]
75    let bytes = blocking::unblock(move || source.fetch()).await?;
76
77    let _permit = RASTER_LIMIT.acquire().await;
78    blocking::unblock(move || {
79        let width = size.width.max(1) as i32;
80        let height = size.height.max(1) as i32;
81
82        let mut dom = svg::Dom::from_bytes(&bytes, FontMgr::empty())
83            .map_err(|err| anyhow::anyhow!("Failed to parse SVG: {err:?}"))?;
84        dom.set_container_size((width, height));
85
86        let mut root = dom.root();
87        root.set_width(svg::Length::new(width as f32, svg::LengthUnit::PX));
88        root.set_height(svg::Length::new(height as f32, svg::LengthUnit::PX));
89        root.set_color(style.color.unwrap_or(Color::BLACK).into());
90        if let Some(fill) = style.fill {
91            root.set_fill(svg::Paint::from_color(fill.into()));
92        }
93        if let Some(stroke) = style.stroke {
94            root.set_stroke(svg::Paint::from_color(stroke.into()));
95        }
96        if let Some(stroke_width) = style.stroke_width {
97            root.set_stroke_width(svg::Length::new(stroke_width, svg::LengthUnit::PX));
98        }
99
100        let mut surface =
101            raster_n32_premul((width, height)).context("Failed to create the SVG surface.")?;
102        dom.render(surface.canvas());
103        Ok(surface.image_snapshot())
104    })
105    .await
106}
107
108/// SVG viewer component.
109///
110/// Rasterizes the SVG asynchronously to the size measured on its container, caching the result.
111/// See [`ImageSource`] for all supported sources.
112///
113/// # Example
114///
115/// ```rust
116/// # use freya::prelude::*;
117/// fn app() -> impl IntoElement {
118///     SvgViewer::new(include_bytes!("../../../examples/ferris.svg"))
119///         .width(Size::px(300.))
120///         .height(Size::px(300.))
121/// }
122/// ```
123#[derive(PartialEq)]
124pub struct SvgViewer {
125    source: ImageSource,
126    asset_age: AssetAge,
127
128    layout: LayoutData,
129    image_data: ImageData,
130    accessibility: AccessibilityData,
131    effect: EffectData,
132    event_handlers: FxHashMap<EventName, EventHandlerType>,
133    style: SvgStyle,
134    show_loader: bool,
135
136    error_renderer: Option<Callback<String, Element>>,
137
138    key: DiffKey,
139}
140
141impl SvgViewer {
142    pub fn new(source: impl Into<ImageSource>) -> Self {
143        let mut accessibility = AccessibilityData::default();
144        accessibility.builder.set_role(AccessibilityRole::SvgRoot);
145
146        SvgViewer {
147            source: source.into(),
148            asset_age: AssetAge::default(),
149            layout: LayoutData::default(),
150            image_data: ImageData::default(),
151            accessibility,
152            effect: EffectData::default(),
153            event_handlers: FxHashMap::default(),
154            style: SvgStyle::default(),
155            show_loader: true,
156            error_renderer: None,
157            key: DiffKey::None,
158        }
159    }
160
161    /// Whether to render a loading indicator while the SVG is being rasterized. Defaults to `true`.
162    pub fn show_loader(mut self, show_loader: bool) -> Self {
163        self.show_loader = show_loader;
164        self
165    }
166
167    /// Override the SVG's `currentColor`, used by shapes that inherit their color.
168    pub fn color(mut self, color: impl Into<Color>) -> Self {
169        self.style.color = Some(color.into());
170        self
171    }
172
173    /// Override the fill color of the SVG's shapes.
174    pub fn fill(mut self, fill: impl Into<Color>) -> Self {
175        self.style.fill = Some(fill.into());
176        self
177    }
178
179    /// Override the stroke color of the SVG's shapes.
180    pub fn stroke(mut self, stroke: impl Into<Color>) -> Self {
181        self.style.stroke = Some(stroke.into());
182        self
183    }
184
185    /// Override the SVG stroke width.
186    pub fn stroke_width(mut self, stroke_width: impl Into<f32>) -> Self {
187        self.style.stroke_width = Some(stroke_width.into());
188        self
189    }
190
191    /// Customize how long the raster remains cached after no longer being used.
192    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
193        self.asset_age = asset_age.into();
194        self
195    }
196
197    /// Custom element rendered when the SVG fails to load.
198    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
199        self.error_renderer = Some(renderer.into());
200        self
201    }
202}
203
204impl KeyExt for SvgViewer {
205    fn write_key(&mut self) -> &mut DiffKey {
206        &mut self.key
207    }
208}
209
210impl LayoutExt for SvgViewer {
211    fn get_layout(&mut self) -> &mut LayoutData {
212        &mut self.layout
213    }
214}
215
216impl ContainerExt for SvgViewer {}
217
218impl ImageExt for SvgViewer {
219    fn get_image_data(&mut self) -> &mut ImageData {
220        &mut self.image_data
221    }
222}
223
224impl AccessibilityExt for SvgViewer {
225    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
226        &mut self.accessibility
227    }
228}
229
230impl EffectExt for SvgViewer {
231    fn get_effect(&mut self) -> &mut EffectData {
232        &mut self.effect
233    }
234}
235
236impl EventHandlersExt for SvgViewer {
237    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
238        &mut self.event_handlers
239    }
240}
241
242/// The logical size implied by a pixel-sized layout.
243fn layout_pixel_size(layout: &LayoutData) -> Option<Size2D> {
244    match (&layout.width, &layout.height) {
245        (Size::Pixels(width), Size::Pixels(height)) => Some(Size2D::new(width.get(), height.get())),
246        _ => None,
247    }
248}
249
250impl Component for SvgViewer {
251    fn render(&self) -> impl IntoElement {
252        let scale_factor = *Platform::get().scale_factor.read();
253        let seed = layout_pixel_size(&self.layout);
254        let mut measured = use_state(|| seed);
255        let mut asset_cacher = use_hook(AssetCacher::get);
256
257        let target = measured().map(|logical| {
258            DecodeSize::new(
259                (logical.width * scale_factor as f32).round().max(1.) as u32,
260                (logical.height * scale_factor as f32).round().max(1.) as u32,
261            )
262        });
263
264        let style = self.style;
265        let asset_config =
266            AssetConfiguration::new((&self.source, target, style.as_key()), self.asset_age);
267        let asset = use_asset(&asset_config);
268
269        use_side_effect_with_deps(
270            &(self.source.clone(), asset_config, target, style),
271            move |(source, asset_config, target, style)| {
272                let Some(target) = *target else {
273                    return;
274                };
275                if matches!(
276                    asset_cacher.read_asset(asset_config),
277                    Some(Asset::Pending) | Some(Asset::Error(_))
278                ) {
279                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
280
281                    let source = source.clone();
282                    let asset_config = asset_config.clone();
283                    let style = *style;
284                    spawn_forever(async move {
285                        match rasterize(source, target, style).await {
286                            Ok(image) => {
287                                asset_cacher.update_asset(
288                                    asset_config,
289                                    Asset::Cached(Rc::new(ImageHandle::new(image, Bytes::new()))),
290                                );
291                            }
292                            Err(err) => {
293                                asset_cacher
294                                    .update_asset(asset_config, Asset::Error(err.to_string()));
295                            }
296                        }
297                    });
298                }
299            },
300        );
301
302        match asset {
303            Asset::Cached(asset) => {
304                let handle = asset.downcast_ref::<ImageHandle>().unwrap().clone();
305                image(handle)
306                    .accessibility(self.accessibility.clone())
307                    .layout(self.layout.clone())
308                    .image_data(self.image_data.clone())
309                    .effect(self.effect.clone())
310                    .with_event_handlers(self.event_handlers.clone())
311                    .on_sized(move |event: Event<SizedEventData>| {
312                        measured.set_if_modified(Some(event.area.size));
313                    })
314                    .into_element()
315            }
316            Asset::Error(err) => match &self.error_renderer {
317                Some(renderer) => renderer.call(err),
318                None => err.into(),
319            },
320            Asset::Pending | Asset::Loading => rect()
321                .layout(self.layout.clone())
322                .on_sized(move |event: Event<SizedEventData>| {
323                    measured.set_if_modified(Some(event.area.size));
324                })
325                .center()
326                .maybe(self.show_loader, |loading| {
327                    loading.child(CircularLoader::new())
328                })
329                .into_element(),
330        }
331    }
332
333    fn render_key(&self) -> DiffKey {
334        self.key.clone().or(self.default_key())
335    }
336}
337
338/// Theme-aware color, fill and stroke shortcuts for [`SvgViewer`].
339pub trait SvgThemeExt {
340    fn theme_color(self) -> Self;
341    fn theme_accent_color(self) -> Self;
342    fn theme_fill(self) -> Self;
343    fn theme_stroke(self) -> Self;
344    fn theme_accent_fill(self) -> Self;
345    fn theme_accent_stroke(self) -> Self;
346}
347
348impl SvgThemeExt for SvgViewer {
349    fn theme_color(self) -> Self {
350        let theme = get_theme_or_default();
351        self.color(theme.read().colors.text_primary)
352    }
353
354    fn theme_accent_color(self) -> Self {
355        let theme = get_theme_or_default();
356        self.color(theme.read().colors.primary)
357    }
358
359    fn theme_fill(self) -> Self {
360        let theme = get_theme_or_default();
361        self.fill(theme.read().colors.text_primary)
362    }
363
364    fn theme_stroke(self) -> Self {
365        let theme = get_theme_or_default();
366        self.stroke(theme.read().colors.text_primary)
367    }
368
369    fn theme_accent_fill(self) -> Self {
370        let theme = get_theme_or_default();
371        self.fill(theme.read().colors.primary)
372    }
373
374    fn theme_accent_stroke(self) -> Self {
375        let theme = get_theme_or_default();
376        self.stroke(theme.read().colors.primary)
377    }
378}