i_slint_renderer_femtovg/
lib.rs1#![doc = include_str!("README.md")]
5#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
6
7use std::cell::{Cell, RefCell};
8use std::num::NonZeroU32;
9use std::pin::Pin;
10use std::rc::{Rc, Weak};
11
12use i_slint_common::sharedfontique;
13use i_slint_core::api::{RenderingNotifier, RenderingState, SetRenderingNotifierError};
14use i_slint_core::graphics::{euclid, rendering_metrics_collector::RenderingMetricsCollector};
15use i_slint_core::graphics::{BorderRadius, Rgba8Pixel};
16use i_slint_core::graphics::{FontRequest, SharedPixelBuffer};
17use i_slint_core::item_rendering::ItemRenderer;
18use i_slint_core::item_tree::ItemTreeWeak;
19use i_slint_core::items::TextWrap;
20use i_slint_core::lengths::{
21 LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor,
22};
23use i_slint_core::platform::PlatformError;
24use i_slint_core::renderer::RendererSealed;
25use i_slint_core::textlayout::sharedparley;
26use i_slint_core::window::{WindowAdapter, WindowInner};
27use i_slint_core::Brush;
28use images::TextureImporter;
29
30type PhysicalLength = euclid::Length<f32, PhysicalPx>;
31type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
32type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
33type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
34type PhysicalBorderRadius = BorderRadius<f32, PhysicalPx>;
35
36use self::itemrenderer::CanvasRc;
37
38mod font_cache;
39mod images;
40mod itemrenderer;
41#[cfg(feature = "opengl")]
42pub mod opengl;
43#[cfg(feature = "wgpu-27")]
44pub mod wgpu;
45
46pub trait WindowSurface<R: femtovg::Renderer> {
47 fn render_surface(&self) -> &R::Surface;
48}
49
50pub trait GraphicsBackend {
51 type Renderer: femtovg::Renderer + TextureImporter;
52 type WindowSurface: WindowSurface<Self::Renderer>;
53 const NAME: &'static str;
54 fn new_suspended() -> Self;
55 fn clear_graphics_context(&self);
56 fn begin_surface_rendering(
57 &self,
58 ) -> Result<Self::WindowSurface, Box<dyn std::error::Error + Send + Sync>>;
59 fn submit_commands(&self, commands: <Self::Renderer as femtovg::Renderer>::CommandBuffer);
60 fn present_surface(
61 &self,
62 surface: Self::WindowSurface,
63 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
64 fn with_graphics_api<R>(
65 &self,
66 callback: impl FnOnce(Option<i_slint_core::api::GraphicsAPI<'_>>) -> R,
67 ) -> Result<R, i_slint_core::platform::PlatformError>;
68 fn resize(
72 &self,
73 width: NonZeroU32,
74 height: NonZeroU32,
75 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
76}
77
78pub struct FemtoVGRenderer<B: GraphicsBackend> {
82 maybe_window_adapter: RefCell<Option<Weak<dyn WindowAdapter>>>,
83 rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
84 canvas: RefCell<Option<CanvasRc<B::Renderer>>>,
85 graphics_cache: itemrenderer::ItemGraphicsCache<B::Renderer>,
86 texture_cache: RefCell<images::TextureCache<B::Renderer>>,
87 rendering_metrics_collector: RefCell<Option<Rc<RenderingMetricsCollector>>>,
88 rendering_first_time: Cell<bool>,
89 graphics_backend: B,
91}
92
93impl<B: GraphicsBackend> FemtoVGRenderer<B> {
94 pub fn render(&self) -> Result<(), i_slint_core::platform::PlatformError> {
96 self.internal_render_with_post_callback(
97 0.,
98 (0., 0.),
99 self.window_adapter()?.window().size(),
100 None,
101 )
102 }
103
104 fn internal_render_with_post_callback(
105 &self,
106 rotation_angle_degrees: f32,
107 translation: (f32, f32),
108 surface_size: i_slint_core::api::PhysicalSize,
109 post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
110 ) -> Result<(), i_slint_core::platform::PlatformError> {
111 let surface = self.graphics_backend.begin_surface_rendering()?;
112
113 if self.rendering_first_time.take() {
114 *self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new(
115 &format!("FemtoVG renderer with {} backend", B::NAME),
116 );
117
118 if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
119 self.with_graphics_api(|api| {
120 callback.notify(RenderingState::RenderingSetup, &api)
121 })?;
122 }
123 }
124
125 let window_adapter = self.window_adapter()?;
126 let window = window_adapter.window();
127 let window_size = window.size();
128
129 let Some((width, height)): Option<(NonZeroU32, NonZeroU32)> =
130 window_size.width.try_into().ok().zip(window_size.height.try_into().ok())
131 else {
132 return Ok(());
134 };
135
136 if self.canvas.borrow().is_none() {
137 return Ok(());
139 }
140
141 let window_inner = WindowInner::from_pub(window);
142 let scale = window_inner.scale_factor().ceil();
143
144 window_inner
145 .draw_contents(|components| -> Result<(), PlatformError> {
146 let canvas = self.canvas.borrow().as_ref().unwrap().clone();
148
149 let window_background_brush =
150 window_inner.window_item().map(|w| w.as_pin_ref().background());
151
152 {
153 let mut femtovg_canvas = canvas.borrow_mut();
154 femtovg_canvas.set_size(surface_size.width, surface_size.height, scale);
158
159 if let Some(Brush::SolidColor(clear_color)) = window_background_brush {
161 femtovg_canvas.clear_rect(
162 0,
163 0,
164 surface_size.width,
165 surface_size.height,
166 self::itemrenderer::to_femtovg_color(&clear_color),
167 );
168 }
169 }
170
171 {
172 let mut femtovg_canvas = canvas.borrow_mut();
173 femtovg_canvas.reset();
174 femtovg_canvas.rotate(rotation_angle_degrees.to_radians());
175 femtovg_canvas.translate(translation.0, translation.1);
176 }
177
178 if let Some(notifier_fn) = self.rendering_notifier.borrow_mut().as_mut() {
179 let mut femtovg_canvas = canvas.borrow_mut();
180 let commands = femtovg_canvas.flush_to_surface(surface.render_surface());
185 self.graphics_backend.submit_commands(commands);
186
187 femtovg_canvas.set_size(width.get(), height.get(), scale);
188 drop(femtovg_canvas);
189
190 self.with_graphics_api(|api| {
191 notifier_fn.notify(RenderingState::BeforeRendering, &api)
192 })?;
193 }
194
195 self.graphics_cache.clear_cache_if_scale_factor_changed(window);
196
197 let mut item_renderer = self::itemrenderer::GLItemRenderer::new(
198 &canvas,
199 &self.graphics_cache,
200 &self.texture_cache,
201 window,
202 width.get(),
203 height.get(),
204 );
205
206 if let Some(window_item_rc) = window_inner.window_item_rc() {
207 let window_item =
208 window_item_rc.downcast::<i_slint_core::items::WindowItem>().unwrap();
209 match window_item.as_pin_ref().background() {
210 Brush::SolidColor(..) => {
211 }
213 _ => {
214 item_renderer.draw_rectangle(
216 window_item.as_pin_ref(),
217 &window_item_rc,
218 i_slint_core::lengths::logical_size_from_api(
219 window.size().to_logical(window_inner.scale_factor()),
220 ),
221 &window_item.as_pin_ref().cached_rendering_data,
222 );
223 }
224 }
225 }
226
227 for (component, origin) in components {
228 if let Some(component) = ItemTreeWeak::upgrade(component) {
229 i_slint_core::item_rendering::render_component_items(
230 &component,
231 &mut item_renderer,
232 *origin,
233 &self.window_adapter()?,
234 );
235 }
236 }
237
238 if let Some(cb) = post_render_cb.as_ref() {
239 cb(&mut item_renderer)
240 }
241
242 if let Some(collector) = &self.rendering_metrics_collector.borrow().as_ref() {
243 let metrics = item_renderer.metrics();
244 collector.measure_frame_rendered(&mut item_renderer, metrics);
245 }
246
247 let commands = canvas.borrow_mut().flush_to_surface(surface.render_surface());
248 self.graphics_backend.submit_commands(commands);
249
250 self.texture_cache.borrow_mut().drain();
253 drop(item_renderer);
254 Ok(())
255 })
256 .unwrap_or(Ok(()))?;
257
258 if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
259 self.with_graphics_api(|api| callback.notify(RenderingState::AfterRendering, &api))?;
260 }
261
262 self.graphics_backend.present_surface(surface)?;
263 Ok(())
264 }
265
266 fn with_graphics_api(
267 &self,
268 callback: impl FnOnce(i_slint_core::api::GraphicsAPI<'_>),
269 ) -> Result<(), PlatformError> {
270 self.graphics_backend.with_graphics_api(|api| callback(api.unwrap()))
271 }
272
273 fn window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
274 self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()).ok_or_else(|| {
275 "Renderer must be associated with component before use".to_string().into()
276 })
277 }
278
279 fn reset_canvas(&self, canvas: CanvasRc<B::Renderer>) {
280 *self.canvas.borrow_mut() = canvas.into();
281 self.rendering_first_time.set(true);
282 }
283}
284
285#[doc(hidden)]
286impl<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
287 fn text_size(
288 &self,
289 font_request: i_slint_core::graphics::FontRequest,
290 text: &str,
291 max_width: Option<LogicalLength>,
292 scale_factor: ScaleFactor,
293 text_wrap: TextWrap,
294 ) -> LogicalSize {
295 sharedparley::text_size(font_request, text, max_width, scale_factor, text_wrap)
296 }
297
298 fn font_metrics(
299 &self,
300 font_request: i_slint_core::graphics::FontRequest,
301 _scale_factor: ScaleFactor,
302 ) -> i_slint_core::items::FontMetrics {
303 sharedparley::font_metrics(font_request)
304 }
305
306 fn text_input_byte_offset_for_position(
307 &self,
308 text_input: Pin<&i_slint_core::items::TextInput>,
309 pos: LogicalPoint,
310 font_request: FontRequest,
311 scale_factor: ScaleFactor,
312 ) -> usize {
313 sharedparley::text_input_byte_offset_for_position(
314 text_input,
315 pos,
316 font_request,
317 scale_factor,
318 )
319 }
320
321 fn text_input_cursor_rect_for_byte_offset(
322 &self,
323 text_input: Pin<&i_slint_core::items::TextInput>,
324 byte_offset: usize,
325 font_request: FontRequest,
326 scale_factor: ScaleFactor,
327 ) -> LogicalRect {
328 sharedparley::text_input_cursor_rect_for_byte_offset(
329 text_input,
330 byte_offset,
331 font_request,
332 scale_factor,
333 )
334 }
335
336 fn register_font_from_memory(
337 &self,
338 data: &'static [u8],
339 ) -> Result<(), Box<dyn std::error::Error>> {
340 sharedfontique::get_collection().register_fonts(data.to_vec().into(), None);
341 Ok(())
342 }
343
344 fn register_font_from_path(
345 &self,
346 path: &std::path::Path,
347 ) -> Result<(), Box<dyn std::error::Error>> {
348 let requested_path = path.canonicalize().unwrap_or_else(|_| path.into());
349 let contents = std::fs::read(requested_path)?;
350 sharedfontique::get_collection().register_fonts(contents.into(), None);
351 Ok(())
352 }
353
354 fn default_font_size(&self) -> LogicalLength {
355 sharedparley::DEFAULT_FONT_SIZE
356 }
357
358 fn set_rendering_notifier(
359 &self,
360 callback: Box<dyn i_slint_core::api::RenderingNotifier>,
361 ) -> Result<(), i_slint_core::api::SetRenderingNotifierError> {
362 let mut notifier = self.rendering_notifier.borrow_mut();
363 if notifier.replace(callback).is_some() {
364 Err(SetRenderingNotifierError::AlreadySet)
365 } else {
366 Ok(())
367 }
368 }
369
370 fn free_graphics_resources(
371 &self,
372 component: i_slint_core::item_tree::ItemTreeRef,
373 _items: &mut dyn Iterator<Item = Pin<i_slint_core::items::ItemRef<'_>>>,
374 ) -> Result<(), i_slint_core::platform::PlatformError> {
375 if !self.graphics_cache.is_empty() {
376 self.graphics_backend.with_graphics_api(|_| {
377 self.graphics_cache.component_destroyed(component);
378 })?;
379 }
380 Ok(())
381 }
382
383 fn set_window_adapter(&self, window_adapter: &Rc<dyn WindowAdapter>) {
384 *self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter));
385 self.graphics_backend
386 .with_graphics_api(|_| {
387 self.graphics_cache.clear_all();
388 self.texture_cache.borrow_mut().clear();
389 })
390 .ok();
391 }
392
393 fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> {
394 if let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok()) {
395 self.graphics_backend.resize(width, height)?;
396 };
397 Ok(())
398 }
399
400 fn take_snapshot(&self) -> Result<SharedPixelBuffer<Rgba8Pixel>, PlatformError> {
402 self.graphics_backend.with_graphics_api(|_| {
403 let Some(canvas) = self.canvas.borrow().as_ref().cloned() else {
404 return Err("FemtoVG renderer cannot take screenshot without a window".into());
405 };
406 let screenshot = canvas
407 .borrow_mut()
408 .screenshot()
409 .map_err(|e| format!("FemtoVG error reading current back buffer: {e}"))?;
410
411 use rgb::ComponentBytes;
412 Ok(SharedPixelBuffer::clone_from_slice(
413 screenshot.buf().as_bytes(),
414 screenshot.width() as u32,
415 screenshot.height() as u32,
416 ))
417 })?
418 }
419
420 fn supports_transformations(&self) -> bool {
421 true
422 }
423}
424
425impl<B: GraphicsBackend> Drop for FemtoVGRenderer<B> {
426 fn drop(&mut self) {
427 self.clear_graphics_context().ok();
428 }
429}
430
431#[doc(hidden)]
434pub trait FemtoVGRendererExt {
435 fn new_suspended() -> Self;
436 fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError>;
437 fn render_transformed_with_post_callback(
438 &self,
439 rotation_angle_degrees: f32,
440 translation: (f32, f32),
441 surface_size: i_slint_core::api::PhysicalSize,
442 post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
443 ) -> Result<(), i_slint_core::platform::PlatformError>;
444}
445
446#[doc(hidden)]
449#[cfg(feature = "opengl")]
450pub trait FemtoVGOpenGLRendererExt {
451 fn set_opengl_context(
452 &self,
453 #[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static,
454 #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement,
455 ) -> Result<(), i_slint_core::platform::PlatformError>;
456}
457
458#[doc(hidden)]
459impl<B: GraphicsBackend> FemtoVGRendererExt for FemtoVGRenderer<B> {
460 fn new_suspended() -> Self {
463 Self {
464 maybe_window_adapter: Default::default(),
465 rendering_notifier: Default::default(),
466 canvas: RefCell::new(None),
467 graphics_cache: Default::default(),
468 texture_cache: Default::default(),
469 rendering_metrics_collector: Default::default(),
470 rendering_first_time: Cell::new(true),
471 graphics_backend: B::new_suspended(),
472 }
473 }
474
475 fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError> {
476 self.graphics_backend.with_graphics_api(|api| {
478 if !self.rendering_first_time.get() && api.is_some() {
480 if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
481 self.with_graphics_api(|api| {
482 callback.notify(RenderingState::RenderingTeardown, &api)
483 })
484 .ok();
485 }
486 }
487
488 self.graphics_cache.clear_all();
489 self.texture_cache.borrow_mut().clear();
490 })?;
491
492 if let Some(canvas) = self.canvas.borrow_mut().take() {
493 if Rc::strong_count(&canvas) != 1 {
494 i_slint_core::debug_log!("internal warning: there are canvas references left when destroying the window. OpenGL resources will be leaked.")
495 }
496 }
497
498 self.graphics_backend.clear_graphics_context();
499
500 Ok(())
501 }
502
503 fn render_transformed_with_post_callback(
504 &self,
505 rotation_angle_degrees: f32,
506 translation: (f32, f32),
507 surface_size: i_slint_core::api::PhysicalSize,
508 post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
509 ) -> Result<(), i_slint_core::platform::PlatformError> {
510 self.internal_render_with_post_callback(
511 rotation_angle_degrees,
512 translation,
513 surface_size,
514 post_render_cb,
515 )
516 }
517}
518
519#[cfg(feature = "opengl")]
520impl FemtoVGOpenGLRendererExt for FemtoVGRenderer<opengl::OpenGLBackend> {
521 fn set_opengl_context(
522 &self,
523 #[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static,
524 #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement,
525 ) -> Result<(), i_slint_core::platform::PlatformError> {
526 self.graphics_backend.set_opengl_context(
527 self,
528 #[cfg(not(target_arch = "wasm32"))]
529 opengl_context,
530 #[cfg(target_arch = "wasm32")]
531 html_canvas,
532 )
533 }
534}
535
536#[cfg(feature = "opengl")]
537pub type FemtoVGOpenGLRenderer = FemtoVGRenderer<opengl::OpenGLBackend>;