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
39static RASTER_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
41
42#[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 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
63async 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#[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 pub fn show_loader(mut self, show_loader: bool) -> Self {
163 self.show_loader = show_loader;
164 self
165 }
166
167 pub fn color(mut self, color: impl Into<Color>) -> Self {
169 self.style.color = Some(color.into());
170 self
171 }
172
173 pub fn fill(mut self, fill: impl Into<Color>) -> Self {
175 self.style.fill = Some(fill.into());
176 self
177 }
178
179 pub fn stroke(mut self, stroke: impl Into<Color>) -> Self {
181 self.style.stroke = Some(stroke.into());
182 self
183 }
184
185 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 pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
193 self.asset_age = asset_age.into();
194 self
195 }
196
197 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
242fn 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
338pub 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}