use std::{
rc::Rc,
sync::LazyLock,
};
use anyhow::Context;
use async_lock::Semaphore;
use bytes::Bytes;
use freya_core::{
element::EventHandlerType,
elements::image::*,
events::name::EventName,
prelude::*,
};
use freya_engine::prelude::{
FontMgr,
SkImage,
raster_n32_premul,
svg,
};
use rustc_hash::FxHashMap;
use torin::prelude::{
Size,
Size2D,
};
#[cfg(feature = "remote-asset")]
use crate::http::Http;
use crate::{
cache::*,
image_viewer::{
DecodeSize,
ImageSource,
},
loader::CircularLoader,
theming::hooks::get_theme_or_default,
};
static RASTER_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
#[derive(Default, Clone, Copy, PartialEq)]
struct SvgStyle {
color: Option<Color>,
fill: Option<Color>,
stroke: Option<Color>,
stroke_width: Option<f32>,
}
impl SvgStyle {
fn as_key(&self) -> (Option<Color>, Option<Color>, Option<Color>, Option<u32>) {
(
self.color,
self.fill,
self.stroke,
self.stroke_width.map(f32::to_bits),
)
}
}
async fn rasterize(
source: ImageSource,
size: DecodeSize,
style: SvgStyle,
) -> anyhow::Result<SkImage> {
#[cfg(feature = "remote-asset")]
let bytes = {
let client = Http::get();
blocking::unblock(move || source.fetch(&client)).await?
};
#[cfg(not(feature = "remote-asset"))]
let bytes = blocking::unblock(move || source.fetch()).await?;
let _permit = RASTER_LIMIT.acquire().await;
blocking::unblock(move || {
let width = size.width.max(1) as i32;
let height = size.height.max(1) as i32;
let mut dom = svg::Dom::from_bytes(&bytes, FontMgr::empty())
.map_err(|err| anyhow::anyhow!("Failed to parse SVG: {err:?}"))?;
dom.set_container_size((width, height));
let mut root = dom.root();
root.set_width(svg::Length::new(width as f32, svg::LengthUnit::PX));
root.set_height(svg::Length::new(height as f32, svg::LengthUnit::PX));
root.set_color(style.color.unwrap_or(Color::BLACK).into());
if let Some(fill) = style.fill {
root.set_fill(svg::Paint::from_color(fill.into()));
}
if let Some(stroke) = style.stroke {
root.set_stroke(svg::Paint::from_color(stroke.into()));
}
if let Some(stroke_width) = style.stroke_width {
root.set_stroke_width(svg::Length::new(stroke_width, svg::LengthUnit::PX));
}
let mut surface =
raster_n32_premul((width, height)).context("Failed to create the SVG surface.")?;
dom.render(surface.canvas());
Ok(surface.image_snapshot())
})
.await
}
#[derive(PartialEq)]
pub struct SvgViewer {
source: ImageSource,
asset_age: AssetAge,
layout: LayoutData,
image_data: ImageData,
accessibility: AccessibilityData,
effect: EffectData,
event_handlers: FxHashMap<EventName, EventHandlerType>,
style: SvgStyle,
show_loader: bool,
error_renderer: Option<Callback<String, Element>>,
key: DiffKey,
}
impl SvgViewer {
pub fn new(source: impl Into<ImageSource>) -> Self {
let mut accessibility = AccessibilityData::default();
accessibility.builder.set_role(AccessibilityRole::SvgRoot);
SvgViewer {
source: source.into(),
asset_age: AssetAge::default(),
layout: LayoutData::default(),
image_data: ImageData::default(),
accessibility,
effect: EffectData::default(),
event_handlers: FxHashMap::default(),
style: SvgStyle::default(),
show_loader: true,
error_renderer: None,
key: DiffKey::None,
}
}
pub fn show_loader(mut self, show_loader: bool) -> Self {
self.show_loader = show_loader;
self
}
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.style.color = Some(color.into());
self
}
pub fn fill(mut self, fill: impl Into<Color>) -> Self {
self.style.fill = Some(fill.into());
self
}
pub fn stroke(mut self, stroke: impl Into<Color>) -> Self {
self.style.stroke = Some(stroke.into());
self
}
pub fn stroke_width(mut self, stroke_width: impl Into<f32>) -> Self {
self.style.stroke_width = Some(stroke_width.into());
self
}
pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
self.asset_age = asset_age.into();
self
}
pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
self.error_renderer = Some(renderer.into());
self
}
}
impl KeyExt for SvgViewer {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl LayoutExt for SvgViewer {
fn get_layout(&mut self) -> &mut LayoutData {
&mut self.layout
}
}
impl ContainerExt for SvgViewer {}
impl ImageExt for SvgViewer {
fn get_image_data(&mut self) -> &mut ImageData {
&mut self.image_data
}
}
impl AccessibilityExt for SvgViewer {
fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
&mut self.accessibility
}
}
impl EffectExt for SvgViewer {
fn get_effect(&mut self) -> &mut EffectData {
&mut self.effect
}
}
impl EventHandlersExt for SvgViewer {
fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
&mut self.event_handlers
}
}
fn layout_pixel_size(layout: &LayoutData) -> Option<Size2D> {
match (&layout.width, &layout.height) {
(Size::Pixels(width), Size::Pixels(height)) => Some(Size2D::new(width.get(), height.get())),
_ => None,
}
}
impl Component for SvgViewer {
fn render(&self) -> impl IntoElement {
let scale_factor = *Platform::get().scale_factor.read();
let seed = layout_pixel_size(&self.layout);
let mut measured = use_state(|| seed);
let mut asset_cacher = use_hook(AssetCacher::get);
let target = measured().map(|logical| {
DecodeSize::new(
(logical.width * scale_factor as f32).round().max(1.) as u32,
(logical.height * scale_factor as f32).round().max(1.) as u32,
)
});
let style = self.style;
let asset_config =
AssetConfiguration::new((&self.source, target, style.as_key()), self.asset_age);
let asset = use_asset(&asset_config);
use_side_effect_with_deps(
&(self.source.clone(), asset_config, target, style),
move |(source, asset_config, target, style)| {
let Some(target) = *target else {
return;
};
if matches!(
asset_cacher.read_asset(asset_config),
Some(Asset::Pending) | Some(Asset::Error(_))
) {
asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
let source = source.clone();
let asset_config = asset_config.clone();
let style = *style;
spawn_forever(async move {
match rasterize(source, target, style).await {
Ok(image) => {
asset_cacher.update_asset(
asset_config,
Asset::Cached(Rc::new(ImageHandle::new(image, Bytes::new()))),
);
}
Err(err) => {
asset_cacher
.update_asset(asset_config, Asset::Error(err.to_string()));
}
}
});
}
},
);
match asset {
Asset::Cached(asset) => {
let handle = asset.downcast_ref::<ImageHandle>().unwrap().clone();
image(handle)
.accessibility(self.accessibility.clone())
.layout(self.layout.clone())
.image_data(self.image_data.clone())
.effect(self.effect.clone())
.with_event_handlers(self.event_handlers.clone())
.on_sized(move |event: Event<SizedEventData>| {
measured.set_if_modified(Some(event.area.size));
})
.into_element()
}
Asset::Error(err) => match &self.error_renderer {
Some(renderer) => renderer.call(err),
None => err.into(),
},
Asset::Pending | Asset::Loading => rect()
.layout(self.layout.clone())
.on_sized(move |event: Event<SizedEventData>| {
measured.set_if_modified(Some(event.area.size));
})
.center()
.maybe(self.show_loader, |loading| {
loading.child(CircularLoader::new())
})
.into_element(),
}
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}
pub trait SvgThemeExt {
fn theme_color(self) -> Self;
fn theme_accent_color(self) -> Self;
fn theme_fill(self) -> Self;
fn theme_stroke(self) -> Self;
fn theme_accent_fill(self) -> Self;
fn theme_accent_stroke(self) -> Self;
}
impl SvgThemeExt for SvgViewer {
fn theme_color(self) -> Self {
let theme = get_theme_or_default();
self.color(theme.read().colors.text_primary)
}
fn theme_accent_color(self) -> Self {
let theme = get_theme_or_default();
self.color(theme.read().colors.primary)
}
fn theme_fill(self) -> Self {
let theme = get_theme_or_default();
self.fill(theme.read().colors.text_primary)
}
fn theme_stroke(self) -> Self {
let theme = get_theme_or_default();
self.stroke(theme.read().colors.text_primary)
}
fn theme_accent_fill(self) -> Self {
let theme = get_theme_or_default();
self.fill(theme.read().colors.primary)
}
fn theme_accent_stroke(self) -> Self {
let theme = get_theme_or_default();
self.stroke(theme.read().colors.primary)
}
}