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/// # Example
115///
116/// ```rust
117/// # use freya::prelude::*;
118/// fn app() -> impl IntoElement {
119///     SvgViewer::new(include_bytes!("../../../examples/ferris.svg"))
120///         .width(Size::px(300.))
121///         .height(Size::px(300.))
122/// }
123/// ```
124#[derive(PartialEq)]
125pub struct SvgViewer {
126    source: ImageSource,
127    asset_age: AssetAge,
128
129    layout: LayoutData,
130    image_data: ImageData,
131    accessibility: AccessibilityData,
132    effect: EffectData,
133    event_handlers: EventHandlers,
134    style: SvgStyle,
135    show_loader: bool,
136    parallel: bool,
137
138    children: Vec<Element>,
139    error_renderer: Option<Callback<String, Element>>,
140
141    key: DiffKey,
142}
143
144impl SvgViewer {
145    pub fn new(source: impl Into<ImageSource>) -> Self {
146        let mut accessibility = AccessibilityData::default();
147        accessibility.builder.set_role(AccessibilityRole::SvgRoot);
148
149        SvgViewer {
150            source: source.into(),
151            asset_age: AssetAge::default(),
152            layout: LayoutData::default(),
153            image_data: ImageData::default(),
154            accessibility,
155            effect: EffectData::default(),
156            event_handlers: EventHandlers::default(),
157            style: SvgStyle::default(),
158            show_loader: true,
159            parallel: false,
160            children: Vec::new(),
161            error_renderer: None,
162            key: DiffKey::None,
163        }
164    }
165
166    /// Whether to render a loading indicator while the SVG is being rasterized. Defaults to `true`.
167    pub fn show_loader(mut self, show_loader: bool) -> Self {
168        self.show_loader = show_loader;
169        self
170    }
171
172    /// Whether to fetch and rasterize the SVG in a background thread. Defaults to `false`.
173    pub fn parallel(mut self, parallel: bool) -> Self {
174        self.parallel = parallel;
175        self
176    }
177
178    /// Override the SVG's `currentColor`, used by shapes that inherit their color.
179    /// When not set, SVGs referencing `currentColor` use the inherited text color.
180    pub fn color(mut self, color: impl Into<Color>) -> Self {
181        self.style.color = Some(color.into());
182        self
183    }
184
185    /// Override the fill color of the SVG's shapes.
186    pub fn fill(mut self, fill: impl Into<Color>) -> Self {
187        self.style.fill = Some(fill.into());
188        self
189    }
190
191    /// Override the stroke color of the SVG's shapes.
192    pub fn stroke(mut self, stroke: impl Into<Color>) -> Self {
193        self.style.stroke = Some(stroke.into());
194        self
195    }
196
197    /// Override the SVG stroke width.
198    pub fn stroke_width(mut self, stroke_width: f32) -> Self {
199        self.style.stroke_width = Some(stroke_width);
200        self
201    }
202
203    /// Customize how long the raster remains cached after no longer being used.
204    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
205        self.asset_age = asset_age.into();
206        self
207    }
208
209    /// Custom element rendered when the SVG fails to load.
210    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
211        self.error_renderer = Some(renderer.into());
212        self
213    }
214}
215
216impl KeyExt for SvgViewer {
217    fn write_key(&mut self) -> &mut DiffKey {
218        &mut self.key
219    }
220}
221
222impl LayoutExt for SvgViewer {
223    fn get_layout(&mut self) -> &mut LayoutData {
224        &mut self.layout
225    }
226}
227
228impl ContainerSizeExt for SvgViewer {}
229impl ContainerWithContentExt for SvgViewer {}
230impl ContainerPositionExt for SvgViewer {}
231
232impl ChildrenExt for SvgViewer {
233    fn get_children(&mut self) -> &mut Vec<Element> {
234        &mut self.children
235    }
236}
237
238impl ImageExt for SvgViewer {
239    fn get_image_data(&mut self) -> &mut ImageData {
240        &mut self.image_data
241    }
242}
243
244impl AccessibilityExt for SvgViewer {
245    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
246        &mut self.accessibility
247    }
248}
249
250impl EffectExt for SvgViewer {
251    fn get_effect(&mut self) -> &mut EffectData {
252        &mut self.effect
253    }
254}
255
256impl EventHandlersExt for SvgViewer {
257    fn get_event_handlers(&mut self) -> &mut EventHandlers {
258        &mut self.event_handlers
259    }
260}
261
262impl Component for SvgViewer {
263    fn render(&self) -> impl IntoElement {
264        let scale_factor = *Platform::get().scale_factor.read();
265        let layout = self.layout.clone();
266        let mut measured = use_state(|| match (&layout.width, &layout.height) {
267            (Size::Pixels(width), Size::Pixels(height)) => {
268                Some(Size2D::new(width.get(), height.get()))
269            }
270            _ => None,
271        });
272        let mut asset_cacher = use_hook(AssetCacher::get);
273        let mut inherited_color = use_state::<Option<Color>>(|| None);
274
275        let target = measured().map(|logical| {
276            DecodeSize::new(
277                (logical.width * scale_factor as f32).round().max(1.) as u32,
278                (logical.height * scale_factor as f32).round().max(1.) as u32,
279            )
280        });
281
282        let mut style = self.style;
283        if style.color.is_none() {
284            style.color = inherited_color();
285        }
286
287        let asset_config =
288            AssetConfiguration::new((&self.source, target, style.as_key()), self.asset_age);
289        use_asset(&asset_config);
290
291        // Rasterize whenever the source, size, style or parallel flag change.
292        let mut previous_configuration = use_state(|| None);
293        if *previous_configuration.peek() != Some((asset_config.clone(), self.parallel)) {
294            previous_configuration.set(Some((asset_config.clone(), self.parallel)));
295
296            if let Some(target) = target
297                && style.color.is_some()
298                && matches!(
299                    asset_cacher.read_asset(&asset_config),
300                    Some(Asset::Pending) | Some(Asset::Error(_))
301                )
302            {
303                asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
304
305                if self.parallel {
306                    let source = self.source.clone();
307                    let asset_config = asset_config.clone();
308                    spawn_forever(async move {
309                        #[cfg(feature = "remote-asset")]
310                        let bytes = {
311                            let client = Http::get();
312                            blocking::unblock(move || source.fetch(&client)).await
313                        };
314                        #[cfg(not(feature = "remote-asset"))]
315                        let bytes = blocking::unblock(move || source.fetch()).await;
316
317                        let result = match bytes {
318                            Ok(bytes) => {
319                                let _permit = RASTER_LIMIT.acquire().await;
320                                blocking::unblock(move || rasterize_bytes(&bytes, target, style))
321                                    .await
322                            }
323                            Err(err) => Err(err),
324                        };
325                        store_raster(asset_cacher, asset_config.clone(), result);
326                    });
327                } else {
328                    #[cfg(feature = "remote-asset")]
329                    let bytes = self.source.clone().fetch(&Http::get());
330                    #[cfg(not(feature = "remote-asset"))]
331                    let bytes = self.source.clone().fetch();
332
333                    let result = bytes.and_then(|bytes| rasterize_bytes(&bytes, target, style));
334                    store_raster(asset_cacher, asset_config.clone(), result);
335                }
336            }
337        }
338
339        let asset = asset_cacher
340            .read_asset(&asset_config)
341            .expect("Asset should exist by now");
342
343        match asset {
344            Asset::Cached(asset) => {
345                let handle = asset.downcast_ref::<ImageHandle>().unwrap().clone();
346                image(handle)
347                    .accessibility(self.accessibility.clone())
348                    .layout(layout)
349                    .image_data(self.image_data.clone())
350                    .effect(self.effect.clone())
351                    .children(self.children.clone())
352                    .event_handlers(self.event_handlers.clone())
353                    .on_sized(move |event: Event<SizedEventData>| {
354                        measured.set_if_modified(Some(event.visible_area.size));
355                    })
356                    .on_styled(move |event: Event<StyledEventData>| {
357                        let color = event.text_style.color.as_color().unwrap_or(Color::BLACK);
358                        inherited_color.set_if_modified(Some(color));
359                    })
360                    .into_element()
361            }
362            Asset::Error(err) => match &self.error_renderer {
363                Some(renderer) => renderer.call(err),
364                None => err.into(),
365            },
366            Asset::Pending | Asset::Loading => rect()
367                .layout(layout)
368                .event_handlers(self.event_handlers.clone())
369                .on_sized(move |event: Event<SizedEventData>| {
370                    measured.set_if_modified(Some(event.visible_area.size));
371                })
372                .on_styled(move |event: Event<StyledEventData>| {
373                    let color = event.text_style.color.as_color().unwrap_or(Color::BLACK);
374                    inherited_color.set_if_modified(Some(color));
375                })
376                .center()
377                .maybe(self.show_loader, |loading| {
378                    loading.child(CircularLoader::new())
379                })
380                .into_element(),
381        }
382    }
383
384    fn render_key(&self) -> DiffKey {
385        self.key.clone().or(self.default_key())
386    }
387}
388
389/// Theme-aware color, fill and stroke shortcuts for [`SvgViewer`].
390pub trait SvgThemeExt {
391    fn theme_color(self) -> Self;
392    fn theme_accent_color(self) -> Self;
393    fn theme_fill(self) -> Self;
394    fn theme_stroke(self) -> Self;
395    fn theme_accent_fill(self) -> Self;
396    fn theme_accent_stroke(self) -> Self;
397}
398
399impl SvgThemeExt for SvgViewer {
400    fn theme_color(self) -> Self {
401        let theme = get_theme_or_default();
402        self.color(theme.read().colors.text_primary)
403    }
404
405    fn theme_accent_color(self) -> Self {
406        let theme = get_theme_or_default();
407        self.color(theme.read().colors.primary)
408    }
409
410    fn theme_fill(self) -> Self {
411        let theme = get_theme_or_default();
412        self.fill(theme.read().colors.text_primary)
413    }
414
415    fn theme_stroke(self) -> Self {
416        let theme = get_theme_or_default();
417        self.stroke(theme.read().colors.text_primary)
418    }
419
420    fn theme_accent_fill(self) -> Self {
421        let theme = get_theme_or_default();
422        self.fill(theme.read().colors.primary)
423    }
424
425    fn theme_accent_stroke(self) -> Self {
426        let theme = get_theme_or_default();
427        self.stroke(theme.read().colors.primary)
428    }
429}