slint/internal/renderers/skia/lib.rs
Olivier Goffart e04f028c91 Fix default-font-size not working with PopupWindow with the Qt backend
We had code on the window to reset the default-font-size property to the
default from the renderer.
For the Qt backend, the PopupWindow being their own Window, this code
was activated also for the PopupWindow's hidden default-font-size
property, which caused all PopupWindow's font to not follow the
default-font-size

Fixes #8855
2025-07-09 17:08:24 +02:00

1079 lines
40 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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")]
#[cfg(any(target_vendor = "apple", skia_backend_vulkan))]
use std::cell::OnceCell;
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
use std::sync::Arc;
use i_slint_core::api::{
GraphicsAPI, PhysicalSize as PhysicalWindowSize, RenderingNotifier, RenderingState,
SetRenderingNotifierError, Window,
};
use i_slint_core::graphics::euclid::{self, Vector2D};
use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector;
use i_slint_core::graphics::RequestedGraphicsAPI;
use i_slint_core::graphics::{BorderRadius, FontRequest, SharedPixelBuffer};
use i_slint_core::item_rendering::{DirtyRegion, ItemCache, ItemRenderer, PartialRenderingState};
use i_slint_core::lengths::{
LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor,
};
use i_slint_core::platform::PlatformError;
use i_slint_core::window::{WindowAdapter, WindowInner};
use i_slint_core::Brush;
type PhysicalLength = euclid::Length<f32, PhysicalPx>;
type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
type PhysicalBorderRadius = BorderRadius<f32, PhysicalPx>;
mod cached_image;
mod itemrenderer;
mod textlayout;
#[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;
use i_slint_core::items::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_software)] {
type DefaultSurface = software_surface::SoftwareSurface;
}
}
fn create_default_surface(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, 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<dyn Surface>),
#[cfg(skia_backend_software)]
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<dyn Surface>)
}
#[cfg(not(skia_backend_software))]
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<PartialRenderingState> {
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<metal_surface::SharedMetalContext>,
#[cfg(skia_backend_vulkan)]
vulkan_context: OnceCell<vulkan_surface::SharedVulkanContext>,
}
/// 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<SkiaSharedContextInner>);
/// 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<Option<Weak<dyn WindowAdapter>>>,
rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
image_cache: ItemCache<Option<skia_safe::Image>>,
path_cache: ItemCache<Option<(Vector2D<f32, PhysicalPx>, skia_safe::Path)>>,
rendering_metrics_collector: RefCell<Option<Rc<RenderingMetricsCollector>>>,
rendering_first_time: Cell<bool>,
surface: RefCell<Option<Box<dyn Surface>>>,
surface_factory: fn(
&SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError>,
pre_present_callback: RefCell<Option<Box<dyn FnMut()>>>,
partial_rendering_state: Option<PartialRenderingState>,
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 {
pub fn default(context: &SkiaSharedContext) -> Self {
Self {
maybe_window_adapter: Default::default(),
rendering_notifier: Default::default(),
image_cache: Default::default(),
path_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(),
path_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<dyn Surface>)
},
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(),
path_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<dyn 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(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(),
path_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<dyn 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_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(),
path_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<dyn 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(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(),
path_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<dyn 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(),
}
}
/// Creates a new renderer is associated with the provided window adapter.
pub fn new(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle>,
size: PhysicalWindowSize,
) -> Result<Self, PlatformError> {
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<dyn Surface + 'static>,
) -> Self {
let partial_rendering_state = create_partial_renderer_state(Some(surface.as_ref())).into();
Self {
maybe_window_adapter: Default::default(),
rendering_notifier: Default::default(),
image_cache: Default::default(),
path_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<dyn Surface + 'static>) {
self.image_cache.clear_all();
self.path_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() {
if 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();
// 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<dyn raw_window_handle::HasWindowHandle>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> 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<(), i_slint_core::platform::PlatformError> {
let window_adapter = self.window_adapter()?;
let size = window_adapter.window().size();
self.internal_render_with_post_callback(0., (0., 0.), size, None)
}
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<(), i_slint_core::platform::PlatformError> {
let surface = self.surface.borrow();
let Some(surface) = surface.as_ref() else { return Ok(()) };
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)
})
}
}
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<DirtyRegion> {
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| {
self.render_components_to_canvas(
skia_canvas,
gr_context,
back_buffer_age,
surface,
window,
post_render_cb,
components,
)
})
.unwrap_or_default();
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
if 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: &[(&i_slint_core::item_tree::ItemTreeRc, LogicalPoint)],
) -> Option<DirtyRegion> {
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);
let mut skia_item_renderer = itemrenderer::SkiaItemRenderer::new(
skia_canvas,
window,
surface,
&self.image_cache,
&self.path_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 = skia_safe::Path::new();
for dirty_rect in partial_renderer.dirty_region.iter() {
let physical_rect = (dirty_rect * scale_factor).to_rect().round_out();
clip_path.add_rect(&to_skia_rect(&physical_rect), None);
}
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::<i_slint_core::items::WindowItem>().unwrap();
match window_item.as_pin_ref().background() {
Brush::SolidColor(clear_color) => {
skia_canvas.clear(itemrenderer::to_skia_color(&clear_color));
}
_ => {
// 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 {
i_slint_core::item_rendering::render_component_items(
component,
item_renderer,
*origin,
&window_adapter,
);
}
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);
if collector.refresh_mode()
== i_slint_core::graphics::rendering_metrics_collector::RefreshMode::FullSpeed
{
if 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<Rc<dyn WindowAdapter>, 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<Box<dyn FnMut()>>) {
*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,
font_request: i_slint_core::graphics::FontRequest,
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
_text_wrap: TextWrap, //TODO: Add support for char-wrap
) -> LogicalSize {
let (layout, _) = textlayout::create_layout(
font_request,
scale_factor,
text,
None,
max_width.map(|w| w * scale_factor),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
None,
);
PhysicalSize::new(layout.max_intrinsic_width().ceil(), layout.height().ceil())
/ scale_factor
}
fn font_metrics(
&self,
font_request: i_slint_core::graphics::FontRequest,
scale_factor: ScaleFactor,
) -> i_slint_core::items::FontMetrics {
textlayout::font_metrics(font_request, scale_factor)
}
fn text_input_byte_offset_for_position(
&self,
text_input: std::pin::Pin<&i_slint_core::items::TextInput>,
pos: LogicalPoint,
font_request: FontRequest,
scale_factor: ScaleFactor,
) -> usize {
let max_width = text_input.width() * scale_factor;
let max_height = text_input.height() * scale_factor;
let pos = pos * scale_factor;
if max_width.get() <= 0. || max_height.get() <= 0. {
return 0;
}
let visual_representation = text_input.visual_representation(None);
let (layout, layout_top_left) = textlayout::create_layout(
font_request,
scale_factor,
&visual_representation.text,
None,
Some(max_width),
max_height,
text_input.horizontal_alignment(),
text_input.vertical_alignment(),
text_input.wrap(),
i_slint_core::items::TextOverflow::Clip,
None,
);
let utf16_index =
layout.get_glyph_position_at_coordinate((pos.x, pos.y - layout_top_left.y)).position;
let mut utf16_count = 0;
let byte_offset = visual_representation
.text
.char_indices()
.find(|(_, x)| {
let r = utf16_count >= utf16_index;
utf16_count += x.len_utf16() as i32;
r
})
.unwrap_or((visual_representation.text.len(), '\0'))
.0;
visual_representation.map_byte_offset_from_byte_offset_in_visual_text(byte_offset)
}
fn text_input_cursor_rect_for_byte_offset(
&self,
text_input: std::pin::Pin<&i_slint_core::items::TextInput>,
byte_offset: usize,
font_request: FontRequest,
scale_factor: ScaleFactor,
) -> LogicalRect {
let max_width = text_input.width() * scale_factor;
let max_height = text_input.height() * scale_factor;
if max_width.get() <= 0. || max_height.get() <= 0. {
return Default::default();
}
let string = text_input.text();
let string = string.as_str();
let (layout, layout_top_left) = textlayout::create_layout(
font_request,
scale_factor,
string,
None,
Some(max_width),
max_height,
text_input.horizontal_alignment(),
text_input.vertical_alignment(),
text_input.wrap(),
i_slint_core::items::TextOverflow::Clip,
None,
);
let physical_cursor_rect = textlayout::cursor_rect(
string,
byte_offset,
layout,
text_input.text_cursor_width() * scale_factor,
text_input.horizontal_alignment(),
);
physical_cursor_rect.translate(layout_top_left.to_vector()) / scale_factor
}
fn register_font_from_memory(
&self,
data: &'static [u8],
) -> Result<(), Box<dyn std::error::Error>> {
textlayout::register_font_from_memory(data)
}
fn register_font_from_path(
&self,
path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
textlayout::register_font_from_path(path)
}
fn set_rendering_notifier(
&self,
callback: Box<dyn RenderingNotifier>,
) -> std::result::Result<(), SetRenderingNotifierError> {
if !self.surface.borrow().as_ref().map_or(DefaultSurface::supports_graphics_api(), |x| {
x.supports_graphics_api_with_self()
}) {
return Err(SetRenderingNotifierError::Unsupported);
}
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<Item = std::pin::Pin<i_slint_core::items::ItemRef<'_>>>,
) -> Result<(), i_slint_core::platform::PlatformError> {
self.image_cache.component_destroyed(component);
self.path_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<dyn WindowAdapter>) {
*self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter));
self.image_cache.clear_all();
self.path_cache.clear_all();
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_rendering_state.clear_cache();
}
}
fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> {
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<SharedPixelBuffer<i_slint_core::graphics::Rgba8Pixel>, 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::<i_slint_core::graphics::Rgba8Pixel>::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: i_slint_core::item_rendering::DirtyRegion) {
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_rendering_state.mark_dirty_region(region);
}
}
}
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<dyn raw_window_handle::HasWindowHandle>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Self, PlatformError>
where
Self: Sized;
/// Returns the name of the surface, for diagnostic purposes.
fn name(&self) -> &'static str;
/// Returns true if the surface supports exposing its platform specific API via the GraphicsAPI struct
/// and the `with_graphics_api` function.
fn supports_graphics_api() -> bool
where
Self: Sized,
{
false
}
fn supports_graphics_api_with_self(&self) -> bool {
false
}
/// 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.
fn render(
&self,
window: &Window,
size: PhysicalWindowSize,
render_callback: &dyn Fn(
&skia_safe::Canvas,
Option<&mut skia_safe::gpu::DirectContext>,
u8,
) -> Option<DirtyRegion>,
pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>,
) -> Result<(), i_slint_core::platform::PlatformError>;
/// 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<u8, PlatformError>;
fn use_partial_rendering(&self) -> bool {
false
}
fn import_opengl_texture(
&self,
_canvas: &skia_safe::Canvas,
_texture: &i_slint_core::graphics::BorrowedOpenGLTexture,
) -> Option<skia_safe::Image> {
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<(), i_slint_core::platform::PlatformError>;
}
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<(), i_slint_core::platform::PlatformError> {
self.internal_render_with_post_callback(
rotation_angle_degrees,
translation,
surface_size,
post_render_cb,
)
}
}