// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #![doc = include_str!("README.md")] #![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")] use std::cell::{Cell, RefCell}; use std::num::NonZeroU32; use std::pin::Pin; use std::rc::{Rc, Weak}; use i_slint_common::sharedfontique; use i_slint_core::api::{RenderingNotifier, RenderingState, SetRenderingNotifierError}; use i_slint_core::graphics::{euclid, rendering_metrics_collector::RenderingMetricsCollector}; use i_slint_core::graphics::{BorderRadius, Rgba8Pixel}; use i_slint_core::graphics::{FontRequest, SharedPixelBuffer}; use i_slint_core::item_rendering::ItemRenderer; use i_slint_core::items::TextWrap; use i_slint_core::lengths::{ LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor, }; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::RendererSealed; use i_slint_core::textlayout::sharedparley; use i_slint_core::window::{WindowAdapter, WindowInner}; use i_slint_core::Brush; use images::TextureImporter; type PhysicalLength = euclid::Length; type PhysicalRect = euclid::Rect; type PhysicalSize = euclid::Size2D; type PhysicalPoint = euclid::Point2D; type PhysicalBorderRadius = BorderRadius; use self::itemrenderer::CanvasRc; mod font_cache; mod images; mod itemrenderer; #[cfg(feature = "opengl")] pub mod opengl; #[cfg(feature = "wgpu-27")] pub mod wgpu; pub trait WindowSurface { fn render_surface(&self) -> &R::Surface; } pub trait GraphicsBackend { type Renderer: femtovg::Renderer + TextureImporter; type WindowSurface: WindowSurface; const NAME: &'static str; fn new_suspended() -> Self; fn clear_graphics_context(&self); fn begin_surface_rendering( &self, ) -> Result>; fn submit_commands(&self, commands: ::CommandBuffer); fn present_surface( &self, surface: Self::WindowSurface, ) -> Result<(), Box>; fn with_graphics_api( &self, callback: impl FnOnce(Option>) -> R, ) -> Result; /// This function is called by the renderers when the surface needs to be resized, typically /// in response to the windowing system notifying of a change in the window system. /// For most implementations this is a no-op, with the exception for wayland for example. fn resize( &self, width: NonZeroU32, height: NonZeroU32, ) -> Result<(), Box>; } /// Use the FemtoVG renderer when implementing a custom Slint platform where you deliver events to /// Slint and want the scene to be rendered using OpenGL. The rendering is done using the [FemtoVG](https://github.com/femtovg/femtovg) /// library. pub struct FemtoVGRenderer { maybe_window_adapter: RefCell>>, rendering_notifier: RefCell>>, canvas: RefCell>>, graphics_cache: itemrenderer::ItemGraphicsCache, texture_cache: RefCell>, rendering_metrics_collector: RefCell>>, rendering_first_time: Cell, // Last field, so that it's dropped last and for example the OpenGL context exists and is current when destroying the FemtoVG canvas graphics_backend: B, } impl FemtoVGRenderer { /// Render the scene using OpenGL. pub fn render(&self) -> Result<(), i_slint_core::platform::PlatformError> { self.internal_render_with_post_callback( 0., (0., 0.), self.window_adapter()?.window().size(), None, ) } fn internal_render_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: i_slint_core::api::PhysicalSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result<(), i_slint_core::platform::PlatformError> { let surface = self.graphics_backend.begin_surface_rendering()?; if self.rendering_first_time.take() { *self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new( &format!("FemtoVG renderer with {} backend", B::NAME), ); if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { self.with_graphics_api(|api| { callback.notify(RenderingState::RenderingSetup, &api) })?; } } let window_adapter = self.window_adapter()?; let window = window_adapter.window(); let window_size = window.size(); let Some((width, height)): Option<(NonZeroU32, NonZeroU32)> = window_size.width.try_into().ok().zip(window_size.height.try_into().ok()) else { // Nothing to render return Ok(()); }; if self.canvas.borrow().is_none() { // Nothing to render return Ok(()); } let window_inner = WindowInner::from_pub(window); let scale = window_inner.scale_factor().ceil(); window_inner .draw_contents(|components| -> Result<(), PlatformError> { // self.canvas is checked for being Some(...) at the beginning of this function let canvas = self.canvas.borrow().as_ref().unwrap().clone(); let window_background_brush = window_inner.window_item().map(|w| w.as_pin_ref().background()); { let mut femtovg_canvas = canvas.borrow_mut(); // We pass an integer that is greater than or equal to the scale factor as // dpi / device pixel ratio as the anti-alias of femtovg needs that to draw text clearly. // We need to care about that `ceil()` when calculating metrics. femtovg_canvas.set_size(surface_size.width, surface_size.height, scale); // Clear with window background if it is a solid color otherwise it will drawn as gradient if let Some(Brush::SolidColor(clear_color)) = window_background_brush { femtovg_canvas.clear_rect( 0, 0, surface_size.width, surface_size.height, self::itemrenderer::to_femtovg_color(&clear_color), ); } } { let mut femtovg_canvas = canvas.borrow_mut(); femtovg_canvas.reset(); femtovg_canvas.rotate(rotation_angle_degrees.to_radians()); femtovg_canvas.translate(translation.0, translation.1); } if let Some(notifier_fn) = self.rendering_notifier.borrow_mut().as_mut() { let mut femtovg_canvas = canvas.borrow_mut(); // For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing // the back buffer, in order to allow the callback to provide its own rendering of the background. // femtovg's clear_rect() will merely schedule a clear call, so flush right away to make it immediate. let commands = femtovg_canvas.flush_to_surface(surface.render_surface()); self.graphics_backend.submit_commands(commands); femtovg_canvas.set_size(width.get(), height.get(), scale); drop(femtovg_canvas); self.with_graphics_api(|api| { notifier_fn.notify(RenderingState::BeforeRendering, &api) })?; } self.graphics_cache.clear_cache_if_scale_factor_changed(window); let mut item_renderer = self::itemrenderer::GLItemRenderer::new( &canvas, &self.graphics_cache, &self.texture_cache, window, width.get(), height.get(), ); if let Some(window_item_rc) = window_inner.window_item_rc() { let window_item = window_item_rc.downcast::().unwrap(); match window_item.as_pin_ref().background() { Brush::SolidColor(..) => { // clear_rect is called earlier } _ => { // Draws the window background as gradient item_renderer.draw_rectangle( window_item.as_pin_ref(), &window_item_rc, i_slint_core::lengths::logical_size_from_api( window.size().to_logical(window_inner.scale_factor()), ), &window_item.as_pin_ref().cached_rendering_data, ); } } } for (component, origin) in components { i_slint_core::item_rendering::render_component_items( component, &mut item_renderer, *origin, &self.window_adapter()?, ); } if let Some(cb) = post_render_cb.as_ref() { cb(&mut item_renderer) } if let Some(collector) = &self.rendering_metrics_collector.borrow().as_ref() { collector.measure_frame_rendered(&mut item_renderer); } let commands = canvas.borrow_mut().flush_to_surface(surface.render_surface()); self.graphics_backend.submit_commands(commands); // Delete any images and layer images (and their FBOs) before making the context not current anymore, to // avoid GPU memory leaks. self.texture_cache.borrow_mut().drain(); drop(item_renderer); Ok(()) }) .unwrap_or(Ok(()))?; if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { self.with_graphics_api(|api| callback.notify(RenderingState::AfterRendering, &api))?; } self.graphics_backend.present_surface(surface)?; Ok(()) } fn with_graphics_api( &self, callback: impl FnOnce(i_slint_core::api::GraphicsAPI<'_>), ) -> Result<(), PlatformError> { self.graphics_backend.with_graphics_api(|api| callback(api.unwrap())) } fn window_adapter(&self) -> Result, PlatformError> { self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()).ok_or_else(|| { "Renderer must be associated with component before use".to_string().into() }) } fn reset_canvas(&self, canvas: CanvasRc) { *self.canvas.borrow_mut() = canvas.into(); self.rendering_first_time.set(true); } } #[doc(hidden)] impl RendererSealed for FemtoVGRenderer { fn text_size( &self, font_request: i_slint_core::graphics::FontRequest, text: &str, max_width: Option, scale_factor: ScaleFactor, text_wrap: TextWrap, ) -> LogicalSize { sharedparley::text_size(font_request, text, max_width, scale_factor, text_wrap) } fn font_metrics( &self, font_request: i_slint_core::graphics::FontRequest, _scale_factor: ScaleFactor, ) -> i_slint_core::items::FontMetrics { sharedparley::font_metrics(font_request) } fn text_input_byte_offset_for_position( &self, text_input: Pin<&i_slint_core::items::TextInput>, pos: LogicalPoint, font_request: FontRequest, scale_factor: ScaleFactor, ) -> usize { sharedparley::text_input_byte_offset_for_position( text_input, pos, font_request, scale_factor, ) } fn text_input_cursor_rect_for_byte_offset( &self, text_input: Pin<&i_slint_core::items::TextInput>, byte_offset: usize, font_request: FontRequest, scale_factor: ScaleFactor, ) -> LogicalRect { sharedparley::text_input_cursor_rect_for_byte_offset( text_input, byte_offset, font_request, scale_factor, ) } fn register_font_from_memory( &self, data: &'static [u8], ) -> Result<(), Box> { sharedfontique::get_collection().register_fonts(data.to_vec().into(), None); Ok(()) } fn register_font_from_path( &self, path: &std::path::Path, ) -> Result<(), Box> { let requested_path = path.canonicalize().unwrap_or_else(|_| path.into()); let contents = std::fs::read(requested_path)?; sharedfontique::get_collection().register_fonts(contents.into(), None); Ok(()) } fn default_font_size(&self) -> LogicalLength { sharedparley::DEFAULT_FONT_SIZE } fn set_rendering_notifier( &self, callback: Box, ) -> Result<(), i_slint_core::api::SetRenderingNotifierError> { let mut notifier = self.rendering_notifier.borrow_mut(); if notifier.replace(callback).is_some() { Err(SetRenderingNotifierError::AlreadySet) } else { Ok(()) } } fn free_graphics_resources( &self, component: i_slint_core::item_tree::ItemTreeRef, _items: &mut dyn Iterator>>, ) -> Result<(), i_slint_core::platform::PlatformError> { if !self.graphics_cache.is_empty() { self.graphics_backend.with_graphics_api(|_| { self.graphics_cache.component_destroyed(component); })?; } Ok(()) } fn set_window_adapter(&self, window_adapter: &Rc) { *self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter)); self.graphics_backend .with_graphics_api(|_| { self.graphics_cache.clear_all(); self.texture_cache.borrow_mut().clear(); }) .ok(); } fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> { if let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok()) { self.graphics_backend.resize(width, height)?; }; Ok(()) } /// Returns an image buffer of what was rendered last by reading the previous front buffer (using glReadPixels). fn take_snapshot(&self) -> Result, PlatformError> { self.graphics_backend.with_graphics_api(|_| { let Some(canvas) = self.canvas.borrow().as_ref().cloned() else { return Err("FemtoVG renderer cannot take screenshot without a window".into()); }; let screenshot = canvas .borrow_mut() .screenshot() .map_err(|e| format!("FemtoVG error reading current back buffer: {e}"))?; use rgb::ComponentBytes; Ok(SharedPixelBuffer::clone_from_slice( screenshot.buf().as_bytes(), screenshot.width() as u32, screenshot.height() as u32, )) })? } fn supports_transformations(&self) -> bool { true } } impl Drop for FemtoVGRenderer { fn drop(&mut self) { self.clear_graphics_context().ok(); } } /// The purpose of this trait is to add internal API that's accessed from the winit/linuxkms backends, but not /// public (as the trait isn't re-exported). #[doc(hidden)] pub trait FemtoVGRendererExt { fn new_suspended() -> Self; fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError>; fn render_transformed_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: i_slint_core::api::PhysicalSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result<(), i_slint_core::platform::PlatformError>; } /// The purpose of this trait is to add internal API specific to the OpenGL renderer that's accessed from the winit /// backend. In this case, the ability to resume a suspended OpenGL renderer by providing a new context. #[doc(hidden)] #[cfg(feature = "opengl")] pub trait FemtoVGOpenGLRendererExt { fn set_opengl_context( &self, #[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static, #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement, ) -> Result<(), i_slint_core::platform::PlatformError>; } #[doc(hidden)] impl FemtoVGRendererExt for FemtoVGRenderer { /// Creates a new renderer in suspended state without OpenGL. Any attempts at rendering, etc. will produce an error, /// until [`Self::set_opengl_context()`] was called successfully. fn new_suspended() -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), canvas: RefCell::new(None), graphics_cache: Default::default(), texture_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Cell::new(true), graphics_backend: B::new_suspended(), } } fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError> { // Ensure the context is current before the renderer is destroyed self.graphics_backend.with_graphics_api(|api| { // If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier. if !self.rendering_first_time.get() && api.is_some() { if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { self.with_graphics_api(|api| { callback.notify(RenderingState::RenderingTeardown, &api) }) .ok(); } } self.graphics_cache.clear_all(); self.texture_cache.borrow_mut().clear(); })?; if let Some(canvas) = self.canvas.borrow_mut().take() { if Rc::strong_count(&canvas) != 1 { i_slint_core::debug_log!("internal warning: there are canvas references left when destroying the window. OpenGL resources will be leaked.") } } self.graphics_backend.clear_graphics_context(); Ok(()) } fn render_transformed_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: i_slint_core::api::PhysicalSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result<(), i_slint_core::platform::PlatformError> { self.internal_render_with_post_callback( rotation_angle_degrees, translation, surface_size, post_render_cb, ) } } #[cfg(feature = "opengl")] impl FemtoVGOpenGLRendererExt for FemtoVGRenderer { fn set_opengl_context( &self, #[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static, #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement, ) -> Result<(), i_slint_core::platform::PlatformError> { self.graphics_backend.set_opengl_context( self, #[cfg(not(target_arch = "wasm32"))] opengl_context, #[cfg(target_arch = "wasm32")] html_canvas, ) } } #[cfg(feature = "opengl")] pub type FemtoVGOpenGLRenderer = FemtoVGRenderer;