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