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