| 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 | |
| 7 | use std::cell::{Cell, RefCell}; |
| 8 | use std::rc::{Rc, Weak}; |
| 9 | |
| 10 | use i_slint_core::api::{ |
| 11 | GraphicsAPI, PhysicalSize as PhysicalWindowSize, RenderingNotifier, RenderingState, |
| 12 | SetRenderingNotifierError, Window, |
| 13 | }; |
| 14 | use i_slint_core::graphics::euclid::{self, Vector2D}; |
| 15 | use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector; |
| 16 | use i_slint_core::graphics::{BorderRadius, FontRequest, RequestedGraphicsAPI, SharedPixelBuffer}; |
| 17 | use i_slint_core::item_rendering::{DirtyRegion, ItemCache, ItemRenderer, PartialRenderingState}; |
| 18 | use i_slint_core::lengths::{ |
| 19 | LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor, |
| 20 | }; |
| 21 | use i_slint_core::platform::PlatformError; |
| 22 | use i_slint_core::window::{WindowAdapter, WindowInner}; |
| 23 | use i_slint_core::Brush; |
| 24 | |
| 25 | type PhysicalLength = euclid::Length<f32, PhysicalPx>; |
| 26 | type PhysicalRect = euclid::Rect<f32, PhysicalPx>; |
| 27 | type PhysicalSize = euclid::Size2D<f32, PhysicalPx>; |
| 28 | type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>; |
| 29 | type PhysicalBorderRadius = BorderRadius<f32, PhysicalPx>; |
| 30 | |
| 31 | mod cached_image; |
| 32 | mod itemrenderer; |
| 33 | mod textlayout; |
| 34 | |
| 35 | #[cfg (skia_backend_software)] |
| 36 | pub mod software_surface; |
| 37 | |
| 38 | #[cfg (target_vendor = "apple" )] |
| 39 | pub mod metal_surface; |
| 40 | |
| 41 | #[cfg (target_family = "windows" )] |
| 42 | pub mod d3d_surface; |
| 43 | |
| 44 | #[cfg (skia_backend_vulkan)] |
| 45 | pub mod vulkan_surface; |
| 46 | |
| 47 | #[cfg (not(target_os = "ios" ))] |
| 48 | pub mod opengl_surface; |
| 49 | |
| 50 | use i_slint_core::items::TextWrap; |
| 51 | use itemrenderer::to_skia_rect; |
| 52 | pub use skia_safe; |
| 53 | |
| 54 | cfg_if::cfg_if! { |
| 55 | if #[cfg(skia_backend_vulkan)] { |
| 56 | type DefaultSurface = vulkan_surface::VulkanSurface; |
| 57 | } else if #[cfg(skia_backend_opengl)] { |
| 58 | type DefaultSurface = opengl_surface::OpenGLSurface; |
| 59 | } else if #[cfg(skia_backend_metal)] { |
| 60 | type DefaultSurface = metal_surface::MetalSurface; |
| 61 | } else if #[cfg(skia_backend_software)] { |
| 62 | type DefaultSurface = software_surface::SoftwareSurface; |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | fn create_default_surface( |
| 67 | window_handle: Rc<dyn raw_window_handle::HasWindowHandle>, |
| 68 | display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>, |
| 69 | size: PhysicalWindowSize, |
| 70 | requested_graphics_api: Option<RequestedGraphicsAPI>, |
| 71 | ) -> Result<Box<dyn Surface>, PlatformError> { |
| 72 | match DefaultSurface::new( |
| 73 | window_handle.clone(), |
| 74 | display_handle.clone(), |
| 75 | size, |
| 76 | requested_graphics_api, |
| 77 | ) { |
| 78 | Ok(gpu_surface: OpenGLSurface) => Ok(Box::new(gpu_surface) as Box<dyn Surface>), |
| 79 | #[cfg (skia_backend_software)] |
| 80 | Err(err: PlatformError) => { |
| 81 | i_slint_core::debug_log!( |
| 82 | "Failed to initialize Skia GPU renderer: {} . Falling back to software rendering" , |
| 83 | err |
| 84 | ); |
| 85 | software_surface::SoftwareSurface::new(window_handle, display_handle, size, None) |
| 86 | .map(|r: SoftwareSurface| Box::new(r) as Box<dyn Surface>) |
| 87 | } |
| 88 | #[cfg (not(skia_backend_software))] |
| 89 | Err(err) => Err(err), |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | enum DirtyRegionDebugMode { |
| 94 | NoDebug, |
| 95 | Visualize, |
| 96 | Log, |
| 97 | } |
| 98 | |
| 99 | impl Default for DirtyRegionDebugMode { |
| 100 | fn default() -> Self { |
| 101 | match std::env::var(key:"SLINT_SKIA_PARTIAL_RENDERING" ).as_deref() { |
| 102 | Ok("visualize" ) => DirtyRegionDebugMode::Visualize, |
| 103 | Ok("log" ) => DirtyRegionDebugMode::Log, |
| 104 | _ => DirtyRegionDebugMode::NoDebug, |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | fn create_partial_renderer_state( |
| 110 | maybe_surface: Option<&dyn Surface>, |
| 111 | ) -> Option<PartialRenderingState> { |
| 112 | maybe_surfacebool |
| 113 | .map_or_else( |
| 114 | || std::env::var("SLINT_SKIA_PARTIAL_RENDERING" ).as_deref().is_ok(), |
| 115 | |surface: &dyn Surface| surface.use_partial_rendering(), |
| 116 | ) |
| 117 | .then(|| PartialRenderingState::default()) |
| 118 | } |
| 119 | |
| 120 | /// Use the SkiaRenderer when implementing a custom Slint platform where you deliver events to |
| 121 | /// Slint and want the scene to be rendered using Skia as underlying graphics library. |
| 122 | pub struct SkiaRenderer { |
| 123 | maybe_window_adapter: RefCell<Option<Weak<dyn WindowAdapter>>>, |
| 124 | rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>, |
| 125 | image_cache: ItemCache<Option<skia_safe::Image>>, |
| 126 | path_cache: ItemCache<Option<(Vector2D<f32, PhysicalPx>, skia_safe::Path)>>, |
| 127 | rendering_metrics_collector: RefCell<Option<Rc<RenderingMetricsCollector>>>, |
| 128 | rendering_first_time: Cell<bool>, |
| 129 | surface: RefCell<Option<Box<dyn Surface>>>, |
| 130 | surface_factory: fn( |
| 131 | window_handle: Rc<dyn raw_window_handle::HasWindowHandle>, |
| 132 | display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>, |
| 133 | size: PhysicalWindowSize, |
| 134 | requested_graphics_api: Option<RequestedGraphicsAPI>, |
| 135 | ) -> Result<Box<dyn Surface>, PlatformError>, |
| 136 | pre_present_callback: RefCell<Option<Box<dyn FnMut()>>>, |
| 137 | partial_rendering_state: Option<PartialRenderingState>, |
| 138 | dirty_region_debug_mode: DirtyRegionDebugMode, |
| 139 | /// Tracking dirty regions indexed by buffer age - 1. More than 3 back buffers aren't supported, but also unlikely to happen. |
| 140 | dirty_region_history: RefCell<[DirtyRegion; 3]>, |
| 141 | } |
| 142 | |
| 143 | impl Default for SkiaRenderer { |
| 144 | fn default() -> Self { |
| 145 | Self { |
| 146 | maybe_window_adapter: Default::default(), |
| 147 | rendering_notifier: Default::default(), |
| 148 | image_cache: Default::default(), |
| 149 | path_cache: Default::default(), |
| 150 | rendering_metrics_collector: Default::default(), |
| 151 | rendering_first_time: Default::default(), |
| 152 | surface: Default::default(), |
| 153 | surface_factory: create_default_surface, |
| 154 | pre_present_callback: Default::default(), |
| 155 | partial_rendering_state: create_partial_renderer_state(maybe_surface:None), |
| 156 | dirty_region_debug_mode: Default::default(), |
| 157 | dirty_region_history: Default::default(), |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | impl SkiaRenderer { |
| 163 | #[cfg (skia_backend_software)] |
| 164 | /// Creates a new SkiaRenderer that will always use Skia's software renderer. |
| 165 | pub fn default_software() -> Self { |
| 166 | Self { |
| 167 | maybe_window_adapter: Default::default(), |
| 168 | rendering_notifier: Default::default(), |
| 169 | image_cache: Default::default(), |
| 170 | path_cache: Default::default(), |
| 171 | rendering_metrics_collector: Default::default(), |
| 172 | rendering_first_time: Default::default(), |
| 173 | surface: Default::default(), |
| 174 | surface_factory: |window_handle, display_handle, size, requested_graphics_api| { |
| 175 | software_surface::SoftwareSurface::new( |
| 176 | window_handle, |
| 177 | display_handle, |
| 178 | size, |
| 179 | requested_graphics_api, |
| 180 | ) |
| 181 | .map(|r| Box::new(r) as Box<dyn Surface>) |
| 182 | }, |
| 183 | pre_present_callback: Default::default(), |
| 184 | partial_rendering_state: PartialRenderingState::default().into(), |
| 185 | dirty_region_debug_mode: Default::default(), |
| 186 | dirty_region_history: Default::default(), |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | #[cfg (not(target_os = "ios" ))] |
| 191 | /// Creates a new SkiaRenderer that will always use Skia's OpenGL renderer. |
| 192 | pub fn default_opengl() -> Self { |
| 193 | Self { |
| 194 | maybe_window_adapter: Default::default(), |
| 195 | rendering_notifier: Default::default(), |
| 196 | image_cache: Default::default(), |
| 197 | path_cache: Default::default(), |
| 198 | rendering_metrics_collector: Default::default(), |
| 199 | rendering_first_time: Default::default(), |
| 200 | surface: Default::default(), |
| 201 | surface_factory: |window_handle, display_handle, size, requested_graphics_api| { |
| 202 | opengl_surface::OpenGLSurface::new( |
| 203 | window_handle, |
| 204 | display_handle, |
| 205 | size, |
| 206 | requested_graphics_api, |
| 207 | ) |
| 208 | .map(|r| Box::new(r) as Box<dyn Surface>) |
| 209 | }, |
| 210 | pre_present_callback: Default::default(), |
| 211 | partial_rendering_state: create_partial_renderer_state(None), |
| 212 | dirty_region_debug_mode: Default::default(), |
| 213 | dirty_region_history: Default::default(), |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | #[cfg (target_vendor = "apple" )] |
| 218 | /// Creates a new SkiaRenderer that will always use Skia's Metal renderer. |
| 219 | pub fn default_metal() -> Self { |
| 220 | Self { |
| 221 | maybe_window_adapter: Default::default(), |
| 222 | rendering_notifier: Default::default(), |
| 223 | image_cache: Default::default(), |
| 224 | path_cache: Default::default(), |
| 225 | rendering_metrics_collector: Default::default(), |
| 226 | rendering_first_time: Default::default(), |
| 227 | surface: Default::default(), |
| 228 | surface_factory: |window_handle, display_handle, size, requested_graphics_api| { |
| 229 | metal_surface::MetalSurface::new( |
| 230 | window_handle, |
| 231 | display_handle, |
| 232 | size, |
| 233 | requested_graphics_api, |
| 234 | ) |
| 235 | .map(|r| Box::new(r) as Box<dyn Surface>) |
| 236 | }, |
| 237 | pre_present_callback: Default::default(), |
| 238 | partial_rendering_state: create_partial_renderer_state(None), |
| 239 | dirty_region_debug_mode: Default::default(), |
| 240 | dirty_region_history: Default::default(), |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | #[cfg (skia_backend_vulkan)] |
| 245 | /// Creates a new SkiaRenderer that will always use Skia's Vulkan renderer. |
| 246 | pub fn default_vulkan() -> Self { |
| 247 | Self { |
| 248 | maybe_window_adapter: Default::default(), |
| 249 | rendering_notifier: Default::default(), |
| 250 | image_cache: Default::default(), |
| 251 | path_cache: Default::default(), |
| 252 | rendering_metrics_collector: Default::default(), |
| 253 | rendering_first_time: Default::default(), |
| 254 | surface: Default::default(), |
| 255 | surface_factory: |window_handle, display_handle, size, requested_graphics_api| { |
| 256 | vulkan_surface::VulkanSurface::new( |
| 257 | window_handle, |
| 258 | display_handle, |
| 259 | size, |
| 260 | requested_graphics_api, |
| 261 | ) |
| 262 | .map(|r| Box::new(r) as Box<dyn Surface>) |
| 263 | }, |
| 264 | pre_present_callback: Default::default(), |
| 265 | partial_rendering_state: create_partial_renderer_state(None), |
| 266 | dirty_region_debug_mode: Default::default(), |
| 267 | dirty_region_history: Default::default(), |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | #[cfg (target_family = "windows" )] |
| 272 | /// Creates a new SkiaRenderer that will always use Skia's Direct3D renderer. |
| 273 | pub fn default_direct3d() -> Self { |
| 274 | Self { |
| 275 | maybe_window_adapter: Default::default(), |
| 276 | rendering_notifier: Default::default(), |
| 277 | image_cache: Default::default(), |
| 278 | path_cache: Default::default(), |
| 279 | rendering_metrics_collector: Default::default(), |
| 280 | rendering_first_time: Default::default(), |
| 281 | surface: Default::default(), |
| 282 | surface_factory: |window_handle, display_handle, size, requested_graphics_api| { |
| 283 | d3d_surface::D3DSurface::new( |
| 284 | window_handle, |
| 285 | display_handle, |
| 286 | size, |
| 287 | requested_graphics_api, |
| 288 | ) |
| 289 | .map(|r| Box::new(r) as Box<dyn Surface>) |
| 290 | }, |
| 291 | pre_present_callback: Default::default(), |
| 292 | partial_rendering_state: create_partial_renderer_state(None), |
| 293 | dirty_region_debug_mode: Default::default(), |
| 294 | dirty_region_history: Default::default(), |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /// Creates a new renderer is associated with the provided window adapter. |
| 299 | pub fn new( |
| 300 | window_handle: Rc<dyn raw_window_handle::HasWindowHandle>, |
| 301 | display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>, |
| 302 | size: PhysicalWindowSize, |
| 303 | ) -> Result<Self, PlatformError> { |
| 304 | Ok(Self::new_with_surface(create_default_surface( |
| 305 | window_handle, |
| 306 | display_handle, |
| 307 | size, |
| 308 | None, |
| 309 | )?)) |
| 310 | } |
| 311 | |
| 312 | /// Creates a new renderer with the given surface trait implementation. |
| 313 | pub fn new_with_surface(surface: Box<dyn Surface + 'static>) -> Self { |
| 314 | let partial_rendering_state = create_partial_renderer_state(Some(surface.as_ref())).into(); |
| 315 | Self { |
| 316 | maybe_window_adapter: Default::default(), |
| 317 | rendering_notifier: Default::default(), |
| 318 | image_cache: Default::default(), |
| 319 | path_cache: Default::default(), |
| 320 | rendering_metrics_collector: Default::default(), |
| 321 | rendering_first_time: Cell::new(true), |
| 322 | surface: RefCell::new(Some(surface)), |
| 323 | surface_factory: |_, _, _, _| { |
| 324 | Err("Skia renderer constructed with surface does not support dynamic surface re-creation" .into()) |
| 325 | }, |
| 326 | pre_present_callback: Default::default(), |
| 327 | partial_rendering_state, |
| 328 | dirty_region_debug_mode: Default::default(), |
| 329 | dirty_region_history: Default::default(), |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | /// Reset the surface to a new surface. (destroy the previously set surface if any) |
| 334 | pub fn set_surface(&self, surface: Box<dyn Surface + 'static>) { |
| 335 | self.image_cache.clear_all(); |
| 336 | self.path_cache.clear_all(); |
| 337 | self.rendering_first_time.set(true); |
| 338 | *self.surface.borrow_mut() = Some(surface); |
| 339 | } |
| 340 | |
| 341 | fn clear_surface(&self) { |
| 342 | let Some(surface) = self.surface.borrow_mut().take() else { |
| 343 | return; |
| 344 | }; |
| 345 | |
| 346 | // If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier. |
| 347 | if !self.rendering_first_time.get() { |
| 348 | if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { |
| 349 | surface |
| 350 | .with_active_surface(&mut || { |
| 351 | surface.with_graphics_api(&mut |api| { |
| 352 | callback.notify(RenderingState::RenderingTeardown, &api) |
| 353 | }) |
| 354 | }) |
| 355 | .ok(); |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | drop(surface); |
| 360 | } |
| 361 | |
| 362 | /// Suspends the renderer by freeing all graphics related resources as well as the underlying |
| 363 | /// rendering surface. Call [`Self::set_window_handle()`] to re-associate the renderer with a new |
| 364 | /// window surface for subsequent rendering. |
| 365 | pub fn suspend(&self) -> Result<(), PlatformError> { |
| 366 | self.image_cache.clear_all(); |
| 367 | self.path_cache.clear_all(); |
| 368 | // Destroy the old surface before allocating the new one, to work around |
| 369 | // the vivante drivers using zwp_linux_explicit_synchronization_v1 and |
| 370 | // trying to create a second synchronization object and that's not allowed. |
| 371 | self.clear_surface(); |
| 372 | Ok(()) |
| 373 | } |
| 374 | |
| 375 | /// Reset the surface to the window given the window handle |
| 376 | pub fn set_window_handle( |
| 377 | &self, |
| 378 | window_handle: Rc<dyn raw_window_handle::HasWindowHandle>, |
| 379 | display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>, |
| 380 | size: PhysicalWindowSize, |
| 381 | requested_graphics_api: Option<RequestedGraphicsAPI>, |
| 382 | ) -> Result<(), PlatformError> { |
| 383 | // just in case |
| 384 | self.suspend()?; |
| 385 | let surface = |
| 386 | (self.surface_factory)(window_handle, display_handle, size, requested_graphics_api)?; |
| 387 | self.set_surface(surface); |
| 388 | Ok(()) |
| 389 | } |
| 390 | |
| 391 | /// Render the scene in the previously associated window. |
| 392 | pub fn render(&self) -> Result<(), i_slint_core::platform::PlatformError> { |
| 393 | let window_adapter = self.window_adapter()?; |
| 394 | let size = window_adapter.window().size(); |
| 395 | self.internal_render_with_post_callback(0., (0., 0.), size, None) |
| 396 | } |
| 397 | |
| 398 | fn internal_render_with_post_callback( |
| 399 | &self, |
| 400 | rotation_angle_degrees: f32, |
| 401 | translation: (f32, f32), |
| 402 | surface_size: PhysicalWindowSize, |
| 403 | post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, |
| 404 | ) -> Result<(), i_slint_core::platform::PlatformError> { |
| 405 | let surface = self.surface.borrow(); |
| 406 | let Some(surface) = surface.as_ref() else { return Ok(()) }; |
| 407 | if self.rendering_first_time.take() { |
| 408 | *self.rendering_metrics_collector.borrow_mut() = |
| 409 | RenderingMetricsCollector::new(&format!( |
| 410 | "Skia renderer (skia backend {}; surface: {} bpp)" , |
| 411 | surface.name(), |
| 412 | surface.bits_per_pixel()? |
| 413 | )); |
| 414 | |
| 415 | if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { |
| 416 | surface.with_graphics_api(&mut |api| { |
| 417 | callback.notify(RenderingState::RenderingSetup, &api) |
| 418 | }) |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | let window_adapter = self.window_adapter()?; |
| 423 | let window = window_adapter.window(); |
| 424 | |
| 425 | surface.render( |
| 426 | window, |
| 427 | surface_size, |
| 428 | &|skia_canvas, gr_context, back_buffer_age| { |
| 429 | self.render_to_canvas( |
| 430 | skia_canvas, |
| 431 | rotation_angle_degrees, |
| 432 | translation, |
| 433 | gr_context, |
| 434 | back_buffer_age, |
| 435 | Some(surface.as_ref()), |
| 436 | window, |
| 437 | post_render_cb, |
| 438 | ) |
| 439 | }, |
| 440 | &self.pre_present_callback, |
| 441 | ) |
| 442 | } |
| 443 | |
| 444 | fn render_to_canvas( |
| 445 | &self, |
| 446 | skia_canvas: &skia_safe::Canvas, |
| 447 | rotation_angle_degrees: f32, |
| 448 | translation: (f32, f32), |
| 449 | gr_context: Option<&mut skia_safe::gpu::DirectContext>, |
| 450 | back_buffer_age: u8, |
| 451 | surface: Option<&dyn Surface>, |
| 452 | window: &i_slint_core::api::Window, |
| 453 | post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, |
| 454 | ) -> Option<DirtyRegion> { |
| 455 | skia_canvas.rotate(rotation_angle_degrees, None); |
| 456 | skia_canvas.translate(translation); |
| 457 | |
| 458 | let window_inner = WindowInner::from_pub(window); |
| 459 | |
| 460 | let dirty_region = window_inner |
| 461 | .draw_contents(|components| { |
| 462 | self.render_components_to_canvas( |
| 463 | skia_canvas, |
| 464 | gr_context, |
| 465 | back_buffer_age, |
| 466 | surface, |
| 467 | window, |
| 468 | post_render_cb, |
| 469 | components, |
| 470 | ) |
| 471 | }) |
| 472 | .unwrap_or_default(); |
| 473 | |
| 474 | if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { |
| 475 | if let Some(surface) = surface { |
| 476 | surface.with_graphics_api(&mut |api| { |
| 477 | callback.notify(RenderingState::AfterRendering, &api) |
| 478 | }) |
| 479 | } |
| 480 | } |
| 481 | |
| 482 | dirty_region |
| 483 | } |
| 484 | |
| 485 | fn render_components_to_canvas( |
| 486 | &self, |
| 487 | skia_canvas: &skia_safe::Canvas, |
| 488 | mut gr_context: Option<&mut skia_safe::gpu::DirectContext>, |
| 489 | back_buffer_age: u8, |
| 490 | surface: Option<&dyn Surface>, |
| 491 | window: &i_slint_core::api::Window, |
| 492 | post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, |
| 493 | components: &[(&i_slint_core::item_tree::ItemTreeRc, LogicalPoint)], |
| 494 | ) -> Option<DirtyRegion> { |
| 495 | let window_inner = WindowInner::from_pub(window); |
| 496 | let window_adapter = window_inner.window_adapter(); |
| 497 | |
| 498 | let mut box_shadow_cache = Default::default(); |
| 499 | |
| 500 | self.image_cache.clear_cache_if_scale_factor_changed(window); |
| 501 | self.path_cache.clear_cache_if_scale_factor_changed(window); |
| 502 | |
| 503 | let mut skia_item_renderer = itemrenderer::SkiaItemRenderer::new( |
| 504 | skia_canvas, |
| 505 | window, |
| 506 | &self.image_cache, |
| 507 | &self.path_cache, |
| 508 | &mut box_shadow_cache, |
| 509 | ); |
| 510 | |
| 511 | let scale_factor = ScaleFactor::new(window_inner.scale_factor()); |
| 512 | let logical_window_size = i_slint_core::lengths::logical_size_from_api( |
| 513 | window.size().to_logical(window_inner.scale_factor()), |
| 514 | ); |
| 515 | |
| 516 | let mut dirty_region = None; |
| 517 | |
| 518 | { |
| 519 | let mut item_renderer: &mut dyn ItemRenderer = &mut skia_item_renderer; |
| 520 | let mut partial_renderer; |
| 521 | let mut dirty_region_to_visualize = None; |
| 522 | |
| 523 | if let Some(partial_rendering_state) = self.partial_rendering_state() { |
| 524 | partial_renderer = |
| 525 | partial_rendering_state.create_partial_renderer(skia_item_renderer); |
| 526 | |
| 527 | let mut dirty_region_history = self.dirty_region_history.borrow_mut(); |
| 528 | |
| 529 | let buffer_dirty_region = if back_buffer_age > 0 |
| 530 | && back_buffer_age as usize - 1 < dirty_region_history.len() |
| 531 | { |
| 532 | // The dirty region is the union of all the previous dirty regions |
| 533 | Some( |
| 534 | dirty_region_history[0..back_buffer_age as usize - 1] |
| 535 | .iter() |
| 536 | .fold(DirtyRegion::default(), |acc, region| acc.union(region)), |
| 537 | ) |
| 538 | } else { |
| 539 | Some(LogicalRect::from_size(logical_window_size).into()) |
| 540 | }; |
| 541 | |
| 542 | let dirty_region_for_this_frame = partial_rendering_state.apply_dirty_region( |
| 543 | &mut partial_renderer, |
| 544 | components, |
| 545 | logical_window_size, |
| 546 | buffer_dirty_region, |
| 547 | ); |
| 548 | |
| 549 | let mut clip_path = skia_safe::Path::new(); |
| 550 | |
| 551 | for dirty_rect in partial_renderer.dirty_region.iter() { |
| 552 | let physical_rect = (dirty_rect * scale_factor).to_rect().round_out(); |
| 553 | clip_path.add_rect(&to_skia_rect(&physical_rect), None); |
| 554 | } |
| 555 | |
| 556 | if matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Log) { |
| 557 | let area_to_repaint: f32 = |
| 558 | partial_renderer.dirty_region.iter().map(|b| b.area()).sum(); |
| 559 | i_slint_core::debug_log!( |
| 560 | "repainting {:.2}%" , |
| 561 | area_to_repaint * 100. / logical_window_size.area() |
| 562 | ); |
| 563 | } |
| 564 | |
| 565 | dirty_region = partial_renderer.dirty_region.clone().into(); |
| 566 | |
| 567 | dirty_region_history.rotate_right(1); |
| 568 | dirty_region_history[0] = dirty_region_for_this_frame; |
| 569 | |
| 570 | skia_canvas.clip_path(&clip_path, None, false); |
| 571 | |
| 572 | if matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Visualize) { |
| 573 | dirty_region_to_visualize = Some(clip_path); |
| 574 | } |
| 575 | |
| 576 | item_renderer = &mut partial_renderer; |
| 577 | } |
| 578 | |
| 579 | if let Some(window_item_rc) = window_inner.window_item_rc() { |
| 580 | let window_item = |
| 581 | window_item_rc.downcast::<i_slint_core::items::WindowItem>().unwrap(); |
| 582 | match window_item.as_pin_ref().background() { |
| 583 | Brush::SolidColor(clear_color) => { |
| 584 | skia_canvas.clear(itemrenderer::to_skia_color(&clear_color)); |
| 585 | } |
| 586 | _ => { |
| 587 | // Draws the window background as gradient |
| 588 | item_renderer.draw_rectangle( |
| 589 | window_item.as_pin_ref(), |
| 590 | &window_item_rc, |
| 591 | i_slint_core::lengths::logical_size_from_api( |
| 592 | window.size().to_logical(window_inner.scale_factor()), |
| 593 | ), |
| 594 | &window_item.as_pin_ref().cached_rendering_data, |
| 595 | ); |
| 596 | } |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() { |
| 601 | // For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing |
| 602 | // the back buffer, in order to allow the callback to provide its own rendering of the background. |
| 603 | // Skia's clear() will merely schedule a clear call, so flush right away to make it immediate. |
| 604 | if let Some(ctx) = gr_context.as_mut() { |
| 605 | ctx.flush(None); |
| 606 | } |
| 607 | |
| 608 | if let Some(surface) = surface { |
| 609 | surface.with_graphics_api(&mut |api| { |
| 610 | callback.notify(RenderingState::BeforeRendering, &api) |
| 611 | }) |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | for (component, origin) in components { |
| 616 | i_slint_core::item_rendering::render_component_items( |
| 617 | component, |
| 618 | item_renderer, |
| 619 | *origin, |
| 620 | &window_adapter, |
| 621 | ); |
| 622 | } |
| 623 | |
| 624 | if let Some(path) = dirty_region_to_visualize { |
| 625 | let mut paint = skia_safe::Paint::new( |
| 626 | &skia_safe::Color4f { a: 0.5, r: 1.0, g: 0., b: 0. }, |
| 627 | None, |
| 628 | ); |
| 629 | paint.set_style(skia_safe::PaintStyle::Stroke); |
| 630 | skia_canvas.draw_path(&path, &paint); |
| 631 | } |
| 632 | |
| 633 | if let Some(collector) = &self.rendering_metrics_collector.borrow_mut().as_ref() { |
| 634 | collector.measure_frame_rendered(item_renderer); |
| 635 | if collector.refresh_mode() |
| 636 | == i_slint_core::graphics::rendering_metrics_collector::RefreshMode::FullSpeed |
| 637 | { |
| 638 | if let Some(partial_rendering_state) = self.partial_rendering_state() { |
| 639 | partial_rendering_state.force_screen_refresh(); |
| 640 | } |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | if let Some(cb) = post_render_cb.as_ref() { |
| 645 | cb(item_renderer) |
| 646 | } |
| 647 | } |
| 648 | |
| 649 | if let Some(ctx) = gr_context.as_mut() { |
| 650 | ctx.flush(None); |
| 651 | } |
| 652 | |
| 653 | dirty_region |
| 654 | } |
| 655 | |
| 656 | fn window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> { |
| 657 | self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()).ok_or_else(|| { |
| 658 | "Renderer must be associated with component before use" .to_string().into() |
| 659 | }) |
| 660 | } |
| 661 | |
| 662 | /// Sets the specified callback, that's invoked before presenting the rendered buffer to the windowing system. |
| 663 | /// This can be useful to implement frame throttling, i.e. for requesting a frame callback from the wayland compositor. |
| 664 | pub fn set_pre_present_callback(&self, callback: Option<Box<dyn FnMut()>>) { |
| 665 | *self.pre_present_callback.borrow_mut() = callback; |
| 666 | } |
| 667 | |
| 668 | fn partial_rendering_state(&self) -> Option<&PartialRenderingState> { |
| 669 | // We don't know where the application might render to, so disable partial rendering. |
| 670 | if self.rendering_notifier.borrow().is_some() { |
| 671 | None |
| 672 | } else { |
| 673 | self.partial_rendering_state.as_ref() |
| 674 | } |
| 675 | } |
| 676 | } |
| 677 | |
| 678 | impl i_slint_core::renderer::RendererSealed for SkiaRenderer { |
| 679 | fn text_size( |
| 680 | &self, |
| 681 | font_request: i_slint_core::graphics::FontRequest, |
| 682 | text: &str, |
| 683 | max_width: Option<LogicalLength>, |
| 684 | scale_factor: ScaleFactor, |
| 685 | _text_wrap: TextWrap, //TODO: Add support for char-wrap |
| 686 | ) -> LogicalSize { |
| 687 | let (layout, _) = textlayout::create_layout( |
| 688 | font_request, |
| 689 | scale_factor, |
| 690 | text, |
| 691 | None, |
| 692 | max_width.map(|w| w * scale_factor), |
| 693 | Default::default(), |
| 694 | Default::default(), |
| 695 | Default::default(), |
| 696 | Default::default(), |
| 697 | Default::default(), |
| 698 | None, |
| 699 | ); |
| 700 | |
| 701 | PhysicalSize::new(layout.max_intrinsic_width().ceil(), layout.height().ceil()) |
| 702 | / scale_factor |
| 703 | } |
| 704 | |
| 705 | fn font_metrics( |
| 706 | &self, |
| 707 | font_request: i_slint_core::graphics::FontRequest, |
| 708 | scale_factor: ScaleFactor, |
| 709 | ) -> i_slint_core::items::FontMetrics { |
| 710 | textlayout::font_metrics(font_request, scale_factor) |
| 711 | } |
| 712 | |
| 713 | fn text_input_byte_offset_for_position( |
| 714 | &self, |
| 715 | text_input: std::pin::Pin<&i_slint_core::items::TextInput>, |
| 716 | pos: LogicalPoint, |
| 717 | font_request: FontRequest, |
| 718 | scale_factor: ScaleFactor, |
| 719 | ) -> usize { |
| 720 | let max_width = text_input.width() * scale_factor; |
| 721 | let max_height = text_input.height() * scale_factor; |
| 722 | let pos = pos * scale_factor; |
| 723 | |
| 724 | if max_width.get() <= 0. || max_height.get() <= 0. { |
| 725 | return 0; |
| 726 | } |
| 727 | |
| 728 | let visual_representation = text_input.visual_representation(None); |
| 729 | |
| 730 | let (layout, layout_top_left) = textlayout::create_layout( |
| 731 | font_request, |
| 732 | scale_factor, |
| 733 | &visual_representation.text, |
| 734 | None, |
| 735 | Some(max_width), |
| 736 | max_height, |
| 737 | text_input.horizontal_alignment(), |
| 738 | text_input.vertical_alignment(), |
| 739 | text_input.wrap(), |
| 740 | i_slint_core::items::TextOverflow::Clip, |
| 741 | None, |
| 742 | ); |
| 743 | |
| 744 | let utf16_index = |
| 745 | layout.get_glyph_position_at_coordinate((pos.x, pos.y - layout_top_left.y)).position; |
| 746 | let mut utf16_count = 0; |
| 747 | let byte_offset = visual_representation |
| 748 | .text |
| 749 | .char_indices() |
| 750 | .find(|(_, x)| { |
| 751 | let r = utf16_count >= utf16_index; |
| 752 | utf16_count += x.len_utf16() as i32; |
| 753 | r |
| 754 | }) |
| 755 | .unwrap_or((visual_representation.text.len(), ' \0' )) |
| 756 | .0; |
| 757 | |
| 758 | visual_representation.map_byte_offset_from_byte_offset_in_visual_text(byte_offset) |
| 759 | } |
| 760 | |
| 761 | fn text_input_cursor_rect_for_byte_offset( |
| 762 | &self, |
| 763 | text_input: std::pin::Pin<&i_slint_core::items::TextInput>, |
| 764 | byte_offset: usize, |
| 765 | font_request: FontRequest, |
| 766 | scale_factor: ScaleFactor, |
| 767 | ) -> LogicalRect { |
| 768 | let max_width = text_input.width() * scale_factor; |
| 769 | let max_height = text_input.height() * scale_factor; |
| 770 | |
| 771 | if max_width.get() <= 0. || max_height.get() <= 0. { |
| 772 | return Default::default(); |
| 773 | } |
| 774 | |
| 775 | let string = text_input.text(); |
| 776 | let string = string.as_str(); |
| 777 | |
| 778 | let (layout, layout_top_left) = textlayout::create_layout( |
| 779 | font_request, |
| 780 | scale_factor, |
| 781 | string, |
| 782 | None, |
| 783 | Some(max_width), |
| 784 | max_height, |
| 785 | text_input.horizontal_alignment(), |
| 786 | text_input.vertical_alignment(), |
| 787 | text_input.wrap(), |
| 788 | i_slint_core::items::TextOverflow::Clip, |
| 789 | None, |
| 790 | ); |
| 791 | |
| 792 | let physical_cursor_rect = textlayout::cursor_rect( |
| 793 | string, |
| 794 | byte_offset, |
| 795 | layout, |
| 796 | text_input.text_cursor_width() * scale_factor, |
| 797 | text_input.horizontal_alignment(), |
| 798 | ); |
| 799 | |
| 800 | physical_cursor_rect.translate(layout_top_left.to_vector()) / scale_factor |
| 801 | } |
| 802 | |
| 803 | fn register_font_from_memory( |
| 804 | &self, |
| 805 | data: &'static [u8], |
| 806 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 807 | textlayout::register_font_from_memory(data) |
| 808 | } |
| 809 | |
| 810 | fn register_font_from_path( |
| 811 | &self, |
| 812 | path: &std::path::Path, |
| 813 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 814 | textlayout::register_font_from_path(path) |
| 815 | } |
| 816 | |
| 817 | fn set_rendering_notifier( |
| 818 | &self, |
| 819 | callback: Box<dyn RenderingNotifier>, |
| 820 | ) -> std::result::Result<(), SetRenderingNotifierError> { |
| 821 | if !self.surface.borrow().as_ref().map_or(DefaultSurface::supports_graphics_api(), |x| { |
| 822 | x.supports_graphics_api_with_self() |
| 823 | }) { |
| 824 | return Err(SetRenderingNotifierError::Unsupported); |
| 825 | } |
| 826 | let mut notifier = self.rendering_notifier.borrow_mut(); |
| 827 | if notifier.replace(callback).is_some() { |
| 828 | Err(SetRenderingNotifierError::AlreadySet) |
| 829 | } else { |
| 830 | Ok(()) |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | fn default_font_size(&self) -> LogicalLength { |
| 835 | self::textlayout::DEFAULT_FONT_SIZE |
| 836 | } |
| 837 | |
| 838 | fn free_graphics_resources( |
| 839 | &self, |
| 840 | component: i_slint_core::item_tree::ItemTreeRef, |
| 841 | items: &mut dyn Iterator<Item = std::pin::Pin<i_slint_core::items::ItemRef<'_>>>, |
| 842 | ) -> Result<(), i_slint_core::platform::PlatformError> { |
| 843 | self.image_cache.component_destroyed(component); |
| 844 | self.path_cache.component_destroyed(component); |
| 845 | |
| 846 | if let Some(partial_rendering_state) = self.partial_rendering_state() { |
| 847 | partial_rendering_state.free_graphics_resources(items); |
| 848 | } |
| 849 | |
| 850 | Ok(()) |
| 851 | } |
| 852 | |
| 853 | fn set_window_adapter(&self, window_adapter: &Rc<dyn WindowAdapter>) { |
| 854 | *self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter)); |
| 855 | self.image_cache.clear_all(); |
| 856 | self.path_cache.clear_all(); |
| 857 | |
| 858 | if let Some(partial_rendering_state) = self.partial_rendering_state() { |
| 859 | partial_rendering_state.clear_cache(); |
| 860 | } |
| 861 | } |
| 862 | |
| 863 | fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> { |
| 864 | if let Some(surface) = self.surface.borrow().as_ref() { |
| 865 | surface.resize_event(size) |
| 866 | } else { |
| 867 | Ok(()) |
| 868 | } |
| 869 | } |
| 870 | |
| 871 | /// Returns an image buffer of what was rendered last by reading the previous front buffer (using glReadPixels). |
| 872 | fn take_snapshot( |
| 873 | &self, |
| 874 | ) -> Result<SharedPixelBuffer<i_slint_core::graphics::Rgba8Pixel>, PlatformError> { |
| 875 | let window_adapter = self.window_adapter()?; |
| 876 | let window = window_adapter.window(); |
| 877 | let size = window_adapter.window().size(); |
| 878 | let (width, height) = (size.width, size.height); |
| 879 | let mut target_buffer = |
| 880 | SharedPixelBuffer::<i_slint_core::graphics::Rgba8Pixel>::new(width, height); |
| 881 | |
| 882 | let mut surface_borrow = skia_safe::surfaces::wrap_pixels( |
| 883 | &skia_safe::ImageInfo::new( |
| 884 | (width as i32, height as i32), |
| 885 | skia_safe::ColorType::RGBA8888, |
| 886 | skia_safe::AlphaType::Opaque, |
| 887 | None, |
| 888 | ), |
| 889 | target_buffer.make_mut_bytes(), |
| 890 | None, |
| 891 | None, |
| 892 | ) |
| 893 | .ok_or_else(|| "Error wrapping target buffer for rendering into with Skia" .to_string())?; |
| 894 | |
| 895 | self.render_to_canvas(surface_borrow.canvas(), 0., (0.0, 0.0), None, 0, None, window, None); |
| 896 | |
| 897 | Ok(target_buffer) |
| 898 | } |
| 899 | |
| 900 | fn mark_dirty_region(&self, region: i_slint_core::item_rendering::DirtyRegion) { |
| 901 | if let Some(partial_rendering_state) = self.partial_rendering_state() { |
| 902 | partial_rendering_state.mark_dirty_region(region); |
| 903 | } |
| 904 | } |
| 905 | } |
| 906 | |
| 907 | impl Drop for SkiaRenderer { |
| 908 | fn drop(&mut self) { |
| 909 | self.clear_surface() |
| 910 | } |
| 911 | } |
| 912 | |
| 913 | /// This trait represents the interface between the Skia renderer and the underlying rendering surface, such as a window |
| 914 | /// with a metal layer, a wayland window with an OpenGL context, etc. |
| 915 | pub trait Surface { |
| 916 | /// Creates a new surface with the given window, display, and size. |
| 917 | fn new( |
| 918 | window_handle: Rc<dyn raw_window_handle::HasWindowHandle>, |
| 919 | display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>, |
| 920 | size: PhysicalWindowSize, |
| 921 | requested_graphics_api: Option<RequestedGraphicsAPI>, |
| 922 | ) -> Result<Self, PlatformError> |
| 923 | where |
| 924 | Self: Sized; |
| 925 | /// Returns the name of the surface, for diagnostic purposes. |
| 926 | fn name(&self) -> &'static str; |
| 927 | /// Returns true if the surface supports exposing its platform specific API via the GraphicsAPI struct |
| 928 | /// and the `with_graphics_api` function. |
| 929 | fn supports_graphics_api() -> bool |
| 930 | where |
| 931 | Self: Sized, |
| 932 | { |
| 933 | false |
| 934 | } |
| 935 | |
| 936 | fn supports_graphics_api_with_self(&self) -> bool { |
| 937 | false |
| 938 | } |
| 939 | |
| 940 | /// If supported, this invokes the specified callback with access to the platform graphics API. |
| 941 | fn with_graphics_api(&self, _callback: &mut dyn FnMut(GraphicsAPI<'_>)) {} |
| 942 | /// Invokes the callback with the surface active. This has only a meaning for OpenGL rendering, where |
| 943 | /// the implementation must make the GL context current. |
| 944 | fn with_active_surface( |
| 945 | &self, |
| 946 | callback: &mut dyn FnMut(), |
| 947 | ) -> Result<(), i_slint_core::platform::PlatformError> { |
| 948 | callback(); |
| 949 | Ok(()) |
| 950 | } |
| 951 | /// Prepares the surface for rendering and invokes the provided callback with access to a Skia canvas and |
| 952 | /// rendering context. |
| 953 | fn render( |
| 954 | &self, |
| 955 | window: &Window, |
| 956 | size: PhysicalWindowSize, |
| 957 | render_callback: &dyn Fn( |
| 958 | &skia_safe::Canvas, |
| 959 | Option<&mut skia_safe::gpu::DirectContext>, |
| 960 | u8, |
| 961 | ) -> Option<DirtyRegion>, |
| 962 | pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>, |
| 963 | ) -> Result<(), i_slint_core::platform::PlatformError>; |
| 964 | /// Called when the surface should be resized. |
| 965 | fn resize_event( |
| 966 | &self, |
| 967 | size: PhysicalWindowSize, |
| 968 | ) -> Result<(), i_slint_core::platform::PlatformError>; |
| 969 | fn bits_per_pixel(&self) -> Result<u8, PlatformError>; |
| 970 | |
| 971 | fn use_partial_rendering(&self) -> bool { |
| 972 | false |
| 973 | } |
| 974 | |
| 975 | /// Implementations should return self to allow upcasting. |
| 976 | fn as_any(&self) -> &dyn core::any::Any { |
| 977 | &() |
| 978 | } |
| 979 | } |
| 980 | |
| 981 | pub trait SkiaRendererExt { |
| 982 | fn render_transformed_with_post_callback( |
| 983 | &self, |
| 984 | rotation_angle_degrees: f32, |
| 985 | translation: (f32, f32), |
| 986 | surface_size: PhysicalWindowSize, |
| 987 | post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, |
| 988 | ) -> Result<(), i_slint_core::platform::PlatformError>; |
| 989 | } |
| 990 | |
| 991 | impl SkiaRendererExt for SkiaRenderer { |
| 992 | fn render_transformed_with_post_callback( |
| 993 | &self, |
| 994 | rotation_angle_degrees: f32, |
| 995 | translation: (f32, f32), |
| 996 | surface_size: PhysicalWindowSize, |
| 997 | post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>, |
| 998 | ) -> Result<(), i_slint_core::platform::PlatformError> { |
| 999 | self.internal_render_with_post_callback( |
| 1000 | rotation_angle_degrees, |
| 1001 | translation, |
| 1002 | surface_size, |
| 1003 | post_render_cb, |
| 1004 | ) |
| 1005 | } |
| 1006 | } |
| 1007 | |