// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 // cSpell: ignore upcasting #![doc = include_str!("README.md")] #![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")] #![cfg_attr(slint_nightly_test, feature(non_exhaustive_omitted_patterns_lint))] #![cfg_attr(slint_nightly_test, warn(non_exhaustive_omitted_patterns))] #[cfg(any(target_vendor = "apple", skia_backend_vulkan))] use std::cell::OnceCell; use std::cell::{Cell, RefCell}; use std::pin::Pin; use std::rc::{Rc, Weak}; use std::sync::Arc; use i_slint_core::Brush; use i_slint_core::api::{ GraphicsAPI, PhysicalSize as PhysicalWindowSize, RenderingNotifier, RenderingState, SetRenderingNotifierError, Window, }; use i_slint_core::graphics::RequestedGraphicsAPI; use i_slint_core::graphics::euclid::{self, Vector2D}; use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector; use i_slint_core::graphics::{BorderRadius, SharedPixelBuffer}; use i_slint_core::item_rendering::{ItemCache, ItemRenderer}; use i_slint_core::item_tree::ItemTreeWeak; use i_slint_core::lengths::{ LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor, }; use i_slint_core::partial_renderer::{DirtyRegion, PartialRenderingState}; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::DrawOutcome; use i_slint_core::textlayout::sharedparley; use i_slint_core::window::{WindowAdapter, WindowInner}; type PhysicalLength = euclid::Length; type PhysicalRect = euclid::Rect; type PhysicalSize = euclid::Size2D; type PhysicalPoint = euclid::Point2D; type PhysicalBorderRadius = BorderRadius; mod cached_image; mod font_cache; mod itemrenderer; #[cfg(skia_backend_software)] pub mod software_surface; #[cfg(target_vendor = "apple")] pub mod metal_surface; #[cfg(target_family = "windows")] pub mod d3d_surface; #[cfg(skia_backend_vulkan)] pub mod vulkan_surface; #[cfg(any(not(target_vendor = "apple"), target_os = "macos"))] pub mod opengl_surface; #[cfg(feature = "wgpu-28")] pub mod wgpu_28_surface; #[cfg(feature = "wgpu-29")] pub mod wgpu_29_surface; #[cfg(feature = "wgpu-29")] mod wgpu_renderer; #[cfg(feature = "wgpu-29")] pub use wgpu_renderer::SkiaWGPURenderer; use i_slint_core::items::{ItemRc, TextWrap}; use itemrenderer::to_skia_rect; pub use skia_safe; cfg_if::cfg_if! { if #[cfg(skia_backend_vulkan)] { type DefaultSurface = vulkan_surface::VulkanSurface; } else if #[cfg(skia_backend_opengl)] { type DefaultSurface = opengl_surface::OpenGLSurface; } else if #[cfg(skia_backend_metal)] { type DefaultSurface = metal_surface::MetalSurface; } else if #[cfg(skia_backend_softbuffer)] { type DefaultSurface = software_surface::SoftwareSurface; } } #[cfg(skia_windowed)] fn create_default_surface( context: &SkiaSharedContext, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result, PlatformError> { match DefaultSurface::new( context, window_handle.clone(), display_handle.clone(), size, requested_graphics_api, ) { Ok(gpu_surface) => Ok(Box::new(gpu_surface) as Box), #[cfg(skia_backend_softbuffer)] Err(err) => { i_slint_core::debug_log!( "Failed to initialize Skia GPU renderer: {} . Falling back to software rendering", err ); software_surface::SoftwareSurface::new( context, window_handle, display_handle, size, None, ) .map(|r| Box::new(r) as Box) } #[cfg(not(skia_backend_softbuffer))] Err(err) => Err(err), } } enum DirtyRegionDebugMode { NoDebug, Visualize, Log, } impl Default for DirtyRegionDebugMode { fn default() -> Self { match std::env::var("SLINT_SKIA_PARTIAL_RENDERING").as_deref() { Ok("visualize") => DirtyRegionDebugMode::Visualize, Ok("log") => DirtyRegionDebugMode::Log, _ => DirtyRegionDebugMode::NoDebug, } } } fn create_partial_renderer_state( maybe_surface: Option<&dyn Surface>, ) -> Option { maybe_surface .map_or_else( || std::env::var("SLINT_SKIA_PARTIAL_RENDERING").as_deref().is_ok(), |surface| surface.use_partial_rendering(), ) .then(PartialRenderingState::default) } #[derive(Default)] struct SkiaSharedContextInner { #[cfg(target_vendor = "apple")] metal_context: OnceCell, #[cfg(skia_backend_vulkan)] vulkan_context: OnceCell, } /// This data structure contains data that's intended to be shared across several instances of SkiaRenderer. /// For example, for Vulkan rendering, this shares the Vulkan instance. /// /// Create an instance once and pass clones of it to the difference constructor functions, to ensure most /// efficient resource usage. #[derive(Clone, Default)] pub struct SkiaSharedContext(#[allow(dead_code)] Rc); /// Use the SkiaRenderer when implementing a custom Slint platform where you deliver events to /// Slint and want the scene to be rendered using Skia as underlying graphics library. pub struct SkiaRenderer { maybe_window_adapter: RefCell>>, rendering_notifier: RefCell>>, image_cache: ItemCache>, layer_cache: ItemCache>, path_cache: ItemCache, skia_safe::Path)>>, text_layout_cache: sharedparley::TextLayoutCache, rendering_metrics_collector: RefCell>>, rendering_first_time: Cell, surface: RefCell>>, surface_factory: fn( &SkiaSharedContext, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result, PlatformError>, pre_present_callback: RefCell>>, partial_rendering_state: Option, dirty_region_debug_mode: DirtyRegionDebugMode, /// Tracking dirty regions indexed by buffer age - 1. More than 3 back buffers aren't supported, but also unlikely to happen. dirty_region_history: RefCell<[DirtyRegion; 3]>, shared_context: SkiaSharedContext, } impl SkiaRenderer { #[cfg(skia_windowed)] pub fn default(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: create_default_surface, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(skia_backend_software)] /// Creates a new SkiaRenderer that will always use Skia's software renderer. pub fn default_software(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { software_surface::SoftwareSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: PartialRenderingState::default().into(), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(any(not(target_vendor = "apple"), target_os = "macos"))] /// Creates a new SkiaRenderer that will always use Skia's OpenGL renderer. pub fn default_opengl(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { opengl_surface::OpenGLSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(target_vendor = "apple")] /// Creates a new SkiaRenderer that will always use Skia's Metal renderer. pub fn default_metal(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { metal_surface::MetalSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(skia_backend_vulkan)] /// Creates a new SkiaRenderer that will always use Skia's Vulkan renderer. pub fn default_vulkan(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { vulkan_surface::VulkanSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(target_family = "windows")] /// Creates a new SkiaRenderer that will always use Skia's Direct3D renderer. pub fn default_direct3d(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { d3d_surface::D3DSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(feature = "unstable-wgpu-28")] /// Creates a new SkiaRenderer that will always use Skia's WGPU 28.x renderer. pub fn default_wgpu_28(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { wgpu_28_surface::WGPUSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } #[cfg(feature = "unstable-wgpu-29")] /// Creates a new SkiaRenderer that will always use Skia's WGPU 29.x renderer. pub fn default_wgpu_29(context: &SkiaSharedContext) -> Self { Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Default::default(), surface: Default::default(), surface_factory: |context, window_handle, display_handle, size, requested_graphics_api| { wgpu_29_surface::WGPUSurface::new( context, window_handle, display_handle, size, requested_graphics_api, ) .map(|r| Box::new(r) as Box) }, pre_present_callback: Default::default(), partial_rendering_state: create_partial_renderer_state(None), dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } /// Creates a new renderer is associated with the provided window adapter. pub fn new( context: &SkiaSharedContext, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, ) -> Result { Ok(Self::new_with_surface( context, create_default_surface(context, window_handle, display_handle, size, None)?, )) } /// Creates a new renderer with the given surface trait implementation. pub fn new_with_surface( context: &SkiaSharedContext, surface: Box, ) -> Self { let partial_rendering_state = create_partial_renderer_state(Some(surface.as_ref())); Self { maybe_window_adapter: Default::default(), rendering_notifier: Default::default(), image_cache: Default::default(), layer_cache: Default::default(), path_cache: Default::default(), text_layout_cache: Default::default(), rendering_metrics_collector: Default::default(), rendering_first_time: Cell::new(true), surface: RefCell::new(Some(surface)), surface_factory: |_, _, _, _, _| { Err("Skia renderer constructed with surface does not support dynamic surface re-creation".into()) }, pre_present_callback: Default::default(), partial_rendering_state, dirty_region_debug_mode: Default::default(), dirty_region_history: Default::default(), shared_context: context.clone(), } } /// Reset the surface to a new surface. (destroy the previously set surface if any) pub fn set_surface(&self, surface: Box) { self.image_cache.clear_all(); self.path_cache.clear_all(); self.text_layout_cache.clear_all(); self.rendering_first_time.set(true); *self.surface.borrow_mut() = Some(surface); } fn clear_surface(&self) { let Some(surface) = self.surface.borrow_mut().take() else { return; }; // If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier. if !self.rendering_first_time.get() && let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { surface .with_active_surface(&mut || { surface.with_graphics_api(&mut |api| { callback.notify(RenderingState::RenderingTeardown, &api) }) }) .ok(); } drop(surface); } /// Suspends the renderer by freeing all graphics related resources as well as the underlying /// rendering surface. Call [`Self::set_window_handle()`] to re-associate the renderer with a new /// window surface for subsequent rendering. pub fn suspend(&self) -> Result<(), PlatformError> { self.image_cache.clear_all(); self.path_cache.clear_all(); self.text_layout_cache.clear_all(); // Destroy the old surface before allocating the new one, to work around // the vivante drivers using zwp_linux_explicit_synchronization_v1 and // trying to create a second synchronization object and that's not allowed. self.clear_surface(); Ok(()) } /// Reset the surface to the window given the window handle pub fn set_window_handle( &self, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result<(), PlatformError> { // just in case self.suspend()?; let surface = (self.surface_factory)( &self.shared_context, window_handle, display_handle, size, requested_graphics_api, )?; self.set_surface(surface); Ok(()) } /// Render the scene in the previously associated window. pub fn render(&self) -> Result { let window_adapter = self.window_adapter()?; let size = window_adapter.window().size(); self.internal_render_with_post_callback(0., (0., 0.), size, None) } fn invoke_rendering_notifier_setup( &self, surface: &dyn Surface, ) -> Result<(), i_slint_core::platform::PlatformError> { if self.rendering_first_time.take() { *self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new(&format!( "Skia renderer (skia backend {}; surface: {} bpp)", surface.name(), surface.bits_per_pixel()? )); if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { surface.with_graphics_api(&mut |api| { callback.notify(RenderingState::RenderingSetup, &api) }) } } Ok(()) } fn internal_render_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: PhysicalWindowSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result { let surface = self.surface.borrow(); let Some(surface) = surface.as_ref() else { return Ok(DrawOutcome::Success) }; self.invoke_rendering_notifier_setup(surface.as_ref())?; let window_adapter = self.window_adapter()?; let window = window_adapter.window(); surface.render( window, surface_size, &|skia_canvas, gr_context, back_buffer_age| { self.render_to_canvas( skia_canvas, rotation_angle_degrees, translation, gr_context, back_buffer_age, Some(surface.as_ref()), window, post_render_cb, ) }, &self.pre_present_callback, ) } fn render_to_canvas( &self, skia_canvas: &skia_safe::Canvas, rotation_angle_degrees: f32, translation: (f32, f32), gr_context: Option<&mut skia_safe::gpu::DirectContext>, back_buffer_age: u8, surface: Option<&dyn Surface>, window: &i_slint_core::api::Window, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Option { skia_canvas.rotate(rotation_angle_degrees, None); skia_canvas.translate(translation); let window_inner = WindowInner::from_pub(window); let dirty_region = window_inner .draw_contents(|components, post_render| { self.render_components_to_canvas( skia_canvas, gr_context, back_buffer_age, surface, window, post_render_cb, components, post_render, ) }) .unwrap_or_default(); if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() && let Some(surface) = surface { surface .with_graphics_api(&mut |api| callback.notify(RenderingState::AfterRendering, &api)) } dirty_region } fn render_components_to_canvas( &self, skia_canvas: &skia_safe::Canvas, mut gr_context: Option<&mut skia_safe::gpu::DirectContext>, back_buffer_age: u8, surface: Option<&dyn Surface>, window: &i_slint_core::api::Window, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, components: &[(ItemTreeWeak, LogicalPoint)], post_render: &dyn Fn(&mut dyn ItemRenderer), ) -> Option { let window_inner = WindowInner::from_pub(window); let window_adapter = window_inner.window_adapter(); let mut box_shadow_cache = Default::default(); self.image_cache.clear_cache_if_scale_factor_changed(window); self.path_cache.clear_cache_if_scale_factor_changed(window); self.text_layout_cache.clear_cache_if_scale_factor_changed(window); let mut skia_item_renderer = itemrenderer::SkiaItemRenderer::new( skia_canvas, window, surface, &self.image_cache, &self.layer_cache, &self.path_cache, &self.text_layout_cache, &mut box_shadow_cache, ); let scale_factor = ScaleFactor::new(window_inner.scale_factor()); let logical_window_size = i_slint_core::lengths::logical_size_from_api( window.size().to_logical(window_inner.scale_factor()), ); let mut dirty_region = None; { let mut item_renderer: &mut dyn ItemRenderer = &mut skia_item_renderer; let mut partial_renderer; let mut dirty_region_to_visualize = None; if let Some(partial_rendering_state) = self.partial_rendering_state() { partial_renderer = partial_rendering_state.create_partial_renderer(skia_item_renderer); let mut dirty_region_history = self.dirty_region_history.borrow_mut(); let buffer_dirty_region = if back_buffer_age > 0 && back_buffer_age as usize - 1 < dirty_region_history.len() { // The dirty region is the union of all the previous dirty regions Some( dirty_region_history[0..back_buffer_age as usize - 1] .iter() .fold(DirtyRegion::default(), |acc, region| acc.union(region)), ) } else { Some(LogicalRect::from_size(logical_window_size).into()) }; let dirty_region_for_this_frame = partial_rendering_state.apply_dirty_region( &mut partial_renderer, components, logical_window_size, buffer_dirty_region, ); let mut clip_path_builder = skia_safe::PathBuilder::new(); for dirty_rect in partial_renderer.dirty_region.iter() { let physical_rect = (dirty_rect * scale_factor).to_rect().round_out(); clip_path_builder.add_rect(to_skia_rect(&physical_rect), None, None); } let clip_path = clip_path_builder.detach(); if matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Log) { let area_to_repaint: f32 = partial_renderer.dirty_region.iter().map(|b| b.area()).sum(); i_slint_core::debug_log!( "repainting {:.2}%", area_to_repaint * 100. / logical_window_size.area() ); } dirty_region = partial_renderer.dirty_region.clone().into(); dirty_region_history.rotate_right(1); dirty_region_history[0] = dirty_region_for_this_frame; skia_canvas.clip_path(&clip_path, None, false); if matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Visualize) { dirty_region_to_visualize = Some(clip_path); } item_renderer = &mut partial_renderer; } if let Some(window_item_rc) = window_inner.window_item_rc() { let window_item = window_item_rc.downcast::().unwrap(); if let Brush::SolidColor(clear_color) = window_item.as_pin_ref().background() { skia_canvas.clear(itemrenderer::to_skia_color(&clear_color)); } else { // 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, ); } } if let Some(callback) = self.rendering_notifier.borrow_mut().as_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. // Skia's clear() will merely schedule a clear call, so flush right away to make it immediate. if let Some(ctx) = gr_context.as_mut() { ctx.flush(None); } if let Some(surface) = surface { surface.with_graphics_api(&mut |api| { callback.notify(RenderingState::BeforeRendering, &api) }) } } for (component, origin) in components { if let Some(component) = ItemTreeWeak::upgrade(component) { i_slint_core::item_rendering::render_component_items( &component, item_renderer, *origin, &window_adapter, ); } } post_render(item_renderer); if let Some(path) = dirty_region_to_visualize { let mut paint = skia_safe::Paint::new( skia_safe::Color4f { a: 0.5, r: 1.0, g: 0., b: 0. }, None, ); paint.set_style(skia_safe::PaintStyle::Stroke); skia_canvas.draw_path(&path, &paint); } if let Some(collector) = &self.rendering_metrics_collector.borrow_mut().as_ref() { collector.measure_frame_rendered(item_renderer, Default::default()); if collector.refresh_mode() == i_slint_core::graphics::rendering_metrics_collector::RefreshMode::FullSpeed && let Some(partial_rendering_state) = self.partial_rendering_state() { partial_rendering_state.force_screen_refresh(); } } if let Some(cb) = post_render_cb.as_ref() { cb(item_renderer) } } if let Some(ctx) = gr_context.as_mut() { ctx.flush(None); } dirty_region } 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() }) } /// Sets the specified callback, that's invoked before presenting the rendered buffer to the windowing system. /// This can be useful to implement frame throttling, i.e. for requesting a frame callback from the wayland compositor. pub fn set_pre_present_callback(&self, callback: Option>) { *self.pre_present_callback.borrow_mut() = callback; } fn partial_rendering_state(&self) -> Option<&PartialRenderingState> { // We don't know where the application might render to, so disable partial rendering. if self.rendering_notifier.borrow().is_some() { None } else { self.partial_rendering_state.as_ref() } } } impl i_slint_core::renderer::RendererSealed for SkiaRenderer { fn text_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::RenderString>, item_rc: &ItemRc, max_width: Option, text_wrap: TextWrap, ) -> LogicalSize { sharedparley::text_size( self, text_item, item_rc, max_width, text_wrap, Some(&self.text_layout_cache), ) .unwrap_or_default() } fn char_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>, item_rc: &i_slint_core::item_tree::ItemRc, ch: char, ) -> LogicalSize { self.slint_context() .and_then(|ctx| { let mut font_ctx = ctx.font_context().borrow_mut(); sharedparley::char_size(&mut font_ctx, text_item, item_rc, ch) }) .unwrap_or_default() } fn font_metrics( &self, font_request: i_slint_core::graphics::FontRequest, ) -> i_slint_core::items::FontMetrics { self.slint_context() .map(|ctx| { let mut font_ctx = ctx.font_context().borrow_mut(); sharedparley::font_metrics(&mut font_ctx, font_request) }) .unwrap_or_default() } fn text_input_byte_offset_for_position( &self, text_input: std::pin::Pin<&i_slint_core::items::TextInput>, item_rc: &i_slint_core::item_tree::ItemRc, pos: LogicalPoint, ) -> usize { sharedparley::text_input_byte_offset_for_position(self, text_input, item_rc, pos) } fn text_input_cursor_rect_for_byte_offset( &self, text_input: std::pin::Pin<&i_slint_core::items::TextInput>, item_rc: &i_slint_core::item_tree::ItemRc, byte_offset: usize, ) -> LogicalRect { sharedparley::text_input_cursor_rect_for_byte_offset(self, text_input, item_rc, byte_offset) } fn register_font_from_memory( &self, data: &'static [u8], ) -> Result<(), Box> { let ctx = self.slint_context().ok_or("slint platform not initialized")?; ctx.font_context().borrow_mut().register_static_font(data); 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)?; let ctx = self.slint_context().ok_or("slint platform not initialized")?; ctx.font_context().borrow_mut().collection.register_fonts(contents.into(), None); Ok(()) } fn set_rendering_notifier( &self, callback: Box, ) -> std::result::Result<(), 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> { self.image_cache.component_destroyed(component); self.path_cache.component_destroyed(component); self.text_layout_cache.component_destroyed(component); if let Some(partial_rendering_state) = self.partial_rendering_state() { partial_rendering_state.free_graphics_resources(items); } Ok(()) } fn set_window_adapter(&self, window_adapter: &Rc) { *self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter)); self.image_cache.clear_all(); self.path_cache.clear_all(); self.text_layout_cache.clear_all(); if let Some(partial_rendering_state) = self.partial_rendering_state() { partial_rendering_state.clear_cache(); } } fn window_adapter(&self) -> Option> { self.maybe_window_adapter .borrow() .as_ref() .and_then(|window_adapter| window_adapter.upgrade()) } fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> { if size.width == 0 || size.height == 0 { return Ok(()); } if let Some(surface) = self.surface.borrow().as_ref() { surface.resize_event(size) } else { Ok(()) } } /// Returns an image buffer of what was rendered last by reading the previous front buffer (using glReadPixels). fn take_snapshot( &self, ) -> Result, PlatformError> { let window_adapter = self.window_adapter()?; let window = window_adapter.window(); let size = window_adapter.window().size(); let (width, height) = (size.width, size.height); let mut target_buffer = SharedPixelBuffer::::new(width, height); let mut surface_borrow = skia_safe::surfaces::wrap_pixels( &skia_safe::ImageInfo::new( (width as i32, height as i32), skia_safe::ColorType::RGBA8888, skia_safe::AlphaType::Opaque, None, ), target_buffer.make_mut_bytes(), None, None, ) .ok_or_else(|| "Error wrapping target buffer for rendering into with Skia".to_string())?; self.render_to_canvas(surface_borrow.canvas(), 0., (0.0, 0.0), None, 0, None, window, None); Ok(target_buffer) } fn mark_dirty_region(&self, region: DirtyRegion) { if let Some(partial_rendering_state) = self.partial_rendering_state() { partial_rendering_state.mark_dirty_region(region); } } fn supports_transformations(&self) -> bool { true } } impl Drop for SkiaRenderer { fn drop(&mut self) { self.clear_surface() } } /// This trait represents the interface between the Skia renderer and the underlying rendering surface, such as a window /// with a metal layer, a wayland window with an OpenGL context, etc. pub trait Surface { /// Creates a new surface with the given window, display, and size. fn new( shared_context: &SkiaSharedContext, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result where Self: Sized; /// Returns the name of the surface, for diagnostic purposes. fn name(&self) -> &'static str; /// If supported, this invokes the specified callback with access to the platform graphics API. fn with_graphics_api(&self, _callback: &mut dyn FnMut(GraphicsAPI<'_>)) {} /// Invokes the callback with the surface active. This has only a meaning for OpenGL rendering, where /// the implementation must make the GL context current. fn with_active_surface( &self, callback: &mut dyn FnMut(), ) -> Result<(), i_slint_core::platform::PlatformError> { callback(); Ok(()) } /// Prepares the surface for rendering and invokes the provided callback with access to a Skia canvas and /// rendering context. Returning `DrawOutcome::Occluded` or `Timeout` lets the caller /// re-arm its redraw flag without rendering. fn render( &self, window: &Window, size: PhysicalWindowSize, render_callback: &dyn Fn( &skia_safe::Canvas, Option<&mut skia_safe::gpu::DirectContext>, u8, ) -> Option, pre_present_callback: &RefCell>>, ) -> Result; /// Called when the surface should be resized. fn resize_event( &self, size: PhysicalWindowSize, ) -> Result<(), i_slint_core::platform::PlatformError>; fn bits_per_pixel(&self) -> Result; fn use_partial_rendering(&self) -> bool { false } fn import_opengl_texture( &self, _canvas: &skia_safe::Canvas, _texture: &i_slint_core::graphics::BorrowedOpenGLTexture, ) -> Option { None } #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))] fn import_wgpu_texture( &self, _canvas: &skia_safe::Canvas, _texture: &i_slint_core::graphics::WGPUTexture, ) -> Option { None } /// Implementations should return self to allow upcasting. fn as_any(&self) -> &dyn core::any::Any { &() } } pub trait SkiaRendererExt { fn render_transformed_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: PhysicalWindowSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result; } impl SkiaRendererExt for SkiaRenderer { fn render_transformed_with_post_callback( &self, rotation_angle_degrees: f32, translation: (f32, f32), surface_size: PhysicalWindowSize, post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, ) -> Result { self.internal_render_with_post_callback( rotation_angle_degrees, translation, surface_size, post_render_cb, ) } }