i_slint_renderer_femtovg/
lib.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4#![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    /// This function is called by the renderers when the surface needs to be resized, typically
69    /// in response to the windowing system notifying of a change in the window system.
70    /// For most implementations this is a no-op, with the exception for wayland for example.
71    fn resize(
72        &self,
73        width: NonZeroU32,
74        height: NonZeroU32,
75    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
76}
77
78/// Use the FemtoVG renderer when implementing a custom Slint platform where you deliver events to
79/// Slint and want the scene to be rendered using OpenGL. The rendering is done using the [FemtoVG](https://github.com/femtovg/femtovg)
80/// library.
81pub 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    // Last field, so that it's dropped last and for example the OpenGL context exists and is current when destroying the FemtoVG canvas
90    graphics_backend: B,
91}
92
93impl<B: GraphicsBackend> FemtoVGRenderer<B> {
94    /// Render the scene using OpenGL.
95    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            // Nothing to render
133            return Ok(());
134        };
135
136        if self.canvas.borrow().is_none() {
137            // Nothing to render
138            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                // self.canvas is checked for being Some(...) at the beginning of this function
147                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                    // We pass an integer that is greater than or equal to the scale factor as
155                    // dpi / device pixel ratio as the anti-alias of femtovg needs that to draw text clearly.
156                    // We need to care about that `ceil()` when calculating metrics.
157                    femtovg_canvas.set_size(surface_size.width, surface_size.height, scale);
158
159                    // Clear with window background if it is a solid color otherwise it will drawn as gradient
160                    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                    // For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing
181                    // the back buffer, in order to allow the callback to provide its own rendering of the background.
182                    // femtovg's clear_rect() will merely schedule a clear call, so flush right away to make it immediate.
183
184                    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                            // clear_rect is called earlier
212                        }
213                        _ => {
214                            // Draws the window background as gradient
215                            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                // Delete any images and layer images (and their FBOs) before making the context not current anymore, to
251                // avoid GPU memory leaks.
252                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    /// Returns an image buffer of what was rendered last by reading the previous front buffer (using glReadPixels).
401    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/// The purpose of this trait is to add internal API that's accessed from the winit/linuxkms backends, but not
432/// public (as the trait isn't re-exported).
433#[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/// The purpose of this trait is to add internal API specific to the OpenGL renderer that's accessed from the winit
447/// backend. In this case, the ability to resume a suspended OpenGL renderer by providing a new context.
448#[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    /// Creates a new renderer in suspended state without OpenGL. Any attempts at rendering, etc. will produce an error,
461    /// until [`Self::set_opengl_context()`] was called successfully.
462    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        // Ensure the context is current before the renderer is destroyed
477        self.graphics_backend.with_graphics_api(|api| {
478            // If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier.
479            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>;