Files
DarkRoom/third_party/i-slint-renderer-skia-1.17.1/itemrenderer.rs
T
dtourolle 4b4c9e2e6d Pre-rotate Slint's Skia drawing on the wgpu swapchain on Android
The other half of the wgpu-hal patch: that one lets a caller promise a
pre-rotated swapchain, and this is the caller keeping the promise.

On configure, `WGPUSurface` reads the surface's `currentTransform`,
sizes the swapchain in the panel's orientation (swapped for a quarter
turn), tells wgpu-hal to use that transform, and before each frame
concatenates the matching rotation onto the Skia canvas. Everything
Slint draws goes through that one matrix, so an imported wgpu texture is
rotated with the rest of the window. Input is not rotated, and must not
be, because Android delivers it in window coordinates.

Three details that would each have been a visible bug:

- `resize_event` compared the new size against the swapchain's. The
  swapchain is transposed while a quarter turn is in effect, so the
  comparison now uses the window's size, kept beside it.
- A half turn, landscape to reverse landscape, changes the transform
  without resizing the window, and wgpu-hal hides the SUBOPTIMAL that
  would report it. So the transform is re-read before every frame. That
  costs one query into the native window.
- The item renderer snapped the origin to the pixel grid only when the
  canvas matrix was a pure translation. Under a rotation that is never
  true, so portrait would have lost pixel alignment everywhere. The check
  now accepts right-angle rotations and flips without scaling.

The direction of each rotation follows the Vulkan spec's reading of
preTransform (the image is drawn already rotated clockwise by the
transform). It has not been confirmed on the device yet.
2026-09-25 04:19:05 -04:00

1284 lines
48 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
// cSpell: ignore rrect skpath
use std::pin::Pin;
use super::{PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalSize};
use i_slint_core::graphics::ApproxEq;
use i_slint_core::graphics::boxshadowcache::BoxShadowCache;
use i_slint_core::graphics::euclid::num::Zero;
use i_slint_core::graphics::euclid::{self, Vector2D};
use i_slint_core::item_rendering::{
CachedRenderingData, ItemCache, ItemRenderer, ItemRendererFeatures, LayerRenderer, RenderImage,
RenderText,
};
use i_slint_core::items::{ImageFit, ImageRendering, ItemRc, Layer, Opacity, RenderingResult};
use i_slint_core::lengths::{
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, LogicalSize,
LogicalVector, PhysicalPx, RectLengths, ScaleFactor, SizeLengths, logical_size_from_api,
};
use i_slint_core::textlayout::sharedparley::{self, GlyphRenderer, fontique};
use i_slint_core::window::WindowInner;
use i_slint_core::{Brush, Color, SharedString};
use skia_safe::{Matrix, TileMode};
pub type SkiaBoxShadowCache = BoxShadowCache<skia_safe::Image>;
#[derive(Clone, Copy)]
struct RenderState {
alpha: f32,
transform: i_slint_core::lengths::ItemTransform,
}
pub struct SkiaItemRenderer<'a> {
pub canvas: &'a skia_safe::Canvas,
pub scale_factor: ScaleFactor,
pub window: &'a i_slint_core::api::Window,
surface: Option<&'a dyn crate::Surface>,
state_stack: Vec<RenderState>,
current_state: RenderState,
image_cache: &'a ItemCache<Option<skia_safe::Image>>,
layer_cache: &'a ItemCache<Option<(PhysicalPoint, skia_safe::Image)>>,
path_cache: &'a ItemCache<Option<(Vector2D<f32, PhysicalPx>, skia_safe::Path)>>,
text_layout_cache: &'a sharedparley::TextLayoutCache,
box_shadow_cache: &'a mut SkiaBoxShadowCache,
}
impl<'a> SkiaItemRenderer<'a> {
pub fn new(
canvas: &'a skia_safe::Canvas,
window: &'a i_slint_core::api::Window,
surface: Option<&'a dyn crate::Surface>,
image_cache: &'a ItemCache<Option<skia_safe::Image>>,
layer_cache: &'a ItemCache<Option<(PhysicalPoint, skia_safe::Image)>>,
path_cache: &'a ItemCache<Option<(Vector2D<f32, PhysicalPx>, skia_safe::Path)>>,
text_layout_cache: &'a sharedparley::TextLayoutCache,
box_shadow_cache: &'a mut SkiaBoxShadowCache,
) -> Self {
Self {
canvas,
scale_factor: ScaleFactor::new(window.scale_factor()),
window,
surface,
state_stack: Vec::new(),
current_state: RenderState {
alpha: 1.0,
transform: i_slint_core::lengths::ItemTransform::identity(),
},
image_cache,
layer_cache,
path_cache,
text_layout_cache,
box_shadow_cache,
}
}
fn default_paint(&self) -> Option<skia_safe::Paint> {
if self.current_state.alpha.approx_eq(&1.0) {
None
} else {
let mut paint = skia_safe::Paint::default();
paint.set_alpha_f(self.current_state.alpha);
Some(paint)
}
}
fn render_drop_shadow_image(
canvas: &skia_safe::Canvas,
shadow_options: &i_slint_core::graphics::boxshadowcache::BoxShadowOptions,
) -> Option<skia_safe::Image> {
let blur = shadow_options.blur.get();
let spread = shadow_options.spread.get();
let shape_w = (shadow_options.width.get() + 2. * spread).max(0.);
let shape_h = (shadow_options.height.get() + 2. * spread).max(0.);
if shape_w <= 0. || shape_h <= 0. {
return None;
}
// CSS rule: outer corner radius after spread = max(0, r + spread).
let shape_radius = (shadow_options.radius + PhysicalBorderRadius::new_uniform(spread))
.max(Default::default());
let canvas_size: skia_safe::Size = (shape_w + 2. * blur, shape_h + 2. * blur).into();
let image_info = skia_safe::ImageInfo::new(
canvas_size.to_ceil(),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
// The shape is centered in the canvas with `blur` padding on all sides so the Gaussian blur
// has room to fade out into transparency.
let rounded_rect = to_skia_rrect(
&PhysicalRect::new(PhysicalPoint::new(blur, blur), PhysicalSize::new(shape_w, shape_h)),
&shape_radius,
);
let mut paint = skia_safe::Paint::default();
paint.set_color(to_skia_color(&shadow_options.color));
paint.set_anti_alias(true);
if blur > 0. {
paint.set_mask_filter(skia_safe::MaskFilter::blur(
skia_safe::BlurStyle::Normal,
blur / 2.,
None,
));
}
let mut surface = canvas.new_surface(&image_info, None)?;
let surface_canvas = surface.canvas();
surface_canvas.clear(skia_safe::Color::TRANSPARENT);
surface_canvas.draw_rrect(rounded_rect, &paint);
Some(surface.image_snapshot())
}
fn render_inset_shadow_image(
canvas: &skia_safe::Canvas,
shadow_options: &i_slint_core::graphics::boxshadowcache::BoxShadowOptions,
) -> Option<skia_safe::Image> {
let width = shadow_options.width.get();
let height = shadow_options.height.get();
if width < 1. || height < 1. {
return None;
}
let blur = shadow_options.blur.get();
let spread = shadow_options.spread.get();
let radius = shadow_options.radius;
let offset_x = shadow_options.offset_x_inset;
let offset_y = shadow_options.offset_y_inset;
// Image is sized to the rectangle's geometry; the geometry rrect serves as the clip so the
// outer blurred edge stays hidden.
let canvas_size = skia_safe::ISize::new(width.ceil() as i32, height.ceil() as i32);
let image_info = skia_safe::ImageInfo::new(
canvas_size,
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
let geometry_rrect = to_skia_rrect(
&PhysicalRect::new(PhysicalPoint::zero(), PhysicalSize::new(width, height)),
&radius,
);
// Inner "hole" rrect: geometry inset by spread on each side, translated by offset.
// CSS: inner radius = max(0, radius - spread).
let inner_rect = skia_safe::Rect::new(
spread + offset_x,
spread + offset_y,
width - spread + offset_x,
height - spread + offset_y,
);
let inner_radius =
(radius - PhysicalBorderRadius::new_uniform(spread)).max(Default::default());
let inner_rrect = to_skia_rrect(
&PhysicalRect::new(
PhysicalPoint::new(inner_rect.left, inner_rect.top),
PhysicalSize::new(inner_rect.width(), inner_rect.height()),
),
&inner_radius,
);
// Outer rect inflated well beyond the geometry so its blurred edge falls outside the clip.
let inflate = blur + spread.abs() + offset_x.abs() + offset_y.abs() + 16.;
let outer_rect =
skia_safe::Rect::new(-inflate, -inflate, width + inflate, height + inflate);
let mut path_builder = skia_safe::PathBuilder::new();
path_builder.set_fill_type(skia_safe::PathFillType::EvenOdd);
path_builder.add_rect(outer_rect, None, None);
path_builder.add_rrect(inner_rrect, None, None);
let path = path_builder.detach();
let mut paint = skia_safe::Paint::default();
paint.set_color(to_skia_color(&shadow_options.color));
paint.set_anti_alias(true);
if blur > 0. {
paint.set_mask_filter(skia_safe::MaskFilter::blur(
skia_safe::BlurStyle::Normal,
blur / 2.,
None,
));
}
let mut surface = canvas.new_surface(&image_info, None)?;
let surface_canvas = surface.canvas();
surface_canvas.clear(skia_safe::Color::TRANSPARENT);
surface_canvas.clip_rrect(geometry_rrect, None, true);
surface_canvas.draw_path(&path, &paint);
Some(surface.image_snapshot())
}
fn brush_to_paint(
&self,
brush: Brush,
width: PhysicalLength,
height: PhysicalLength,
) -> Option<skia_safe::Paint> {
let (mut paint, shader) = Self::brush_to_shader(
self.default_paint().unwrap_or_default(),
brush,
width,
height,
self.scale_factor.get(),
)?;
paint.set_shader(Some(shader));
Some(paint)
}
fn brush_to_shader(
mut paint: skia_safe::Paint,
brush: Brush,
width: PhysicalLength,
height: PhysicalLength,
scale_factor: f32,
) -> Option<(skia_safe::Paint, skia_safe::Shader)> {
if brush.is_transparent() {
return None;
}
match brush {
Brush::SolidColor(color) => Some(skia_safe::shaders::color(to_skia_color(&color))),
Brush::LinearGradient(g) => {
let (start, end) = i_slint_core::graphics::line_for_angle(
g.angle(),
[width.get(), height.get()].into(),
);
let (colors, pos): (Vec<skia_safe::Color4f>, Vec<_>) = g
.stops()
.map(|s| (skia_safe::Color4f::from(to_skia_color(&s.color)), s.position))
.unzip();
paint.set_dither(true);
let gradient_colors =
skia_safe::gradient::Colors::new(&colors, Some(&*pos), TileMode::Clamp, None);
let gradient = skia_safe::gradient::Gradient::new(
gradient_colors,
skia_safe::gradient::Interpolation {
in_premul: skia_safe::gradient::interpolation::InPremul::Yes,
..Default::default()
},
);
skia_safe::gradient::shaders::linear_gradient(
(skia_safe::Point::new(start.x, start.y), skia_safe::Point::new(end.x, end.y)),
&gradient,
None,
)
}
Brush::RadialGradient(g) => {
let (colors, pos): (Vec<skia_safe::Color4f>, Vec<_>) = g
.stops()
.map(|s| (skia_safe::Color4f::from(to_skia_color(&s.color)), s.position))
.unzip();
let (cx, cy) = g.center_or_default_scaled(width.get(), height.get(), scale_factor);
let circle_scale =
g.radius_or_default_scaled(width.get(), height.get(), scale_factor);
paint.set_dither(true);
let gradient_colors =
skia_safe::gradient::Colors::new(&colors, Some(&*pos), TileMode::Clamp, None);
let gradient = skia_safe::gradient::Gradient::new(
gradient_colors,
skia_safe::gradient::Interpolation {
in_premul: skia_safe::gradient::interpolation::InPremul::Yes,
..Default::default()
},
);
let mut local_matrix = skia_safe::Matrix::scale((circle_scale, circle_scale));
local_matrix.post_translate((cx, cy));
skia_safe::gradient::shaders::radial_gradient(
(skia_safe::Point::new(0., 0.), 1.),
&gradient,
&local_matrix,
)
}
Brush::ConicGradient(g) => {
let (colors, pos): (Vec<skia_safe::Color4f>, Vec<_>) = g
.stops()
.map(|s| (skia_safe::Color4f::from(to_skia_color(&s.color)), s.position))
.unzip();
let (cx, cy) = g.center_or_default_scaled(width.get(), height.get(), scale_factor);
paint.set_dither(true);
// Skia's sweep gradient uses 0 degrees at 3 o'clock (east)
// We want 0 degrees at 12 o'clock (north), so we need to rotate by -90 degrees
let center = skia_safe::Point::new(cx, cy);
let gradient_colors =
skia_safe::gradient::Colors::new(&colors, Some(&*pos), TileMode::Clamp, None);
let gradient = skia_safe::gradient::Gradient::new(
gradient_colors,
skia_safe::gradient::Interpolation::default(),
);
skia_safe::gradient::shaders::sweep_gradient(
center,
(0.0, 360.0),
&gradient,
&skia_safe::Matrix::rotate_deg_pivot(-90.0, center),
)
}
_ => None,
}
.map(|shader| (paint, shader))
}
fn colorize_image(
&mut self,
image: skia_safe::Image,
colorize_brush: Brush,
) -> Option<skia_safe::Image> {
let image_info = skia_safe::ImageInfo::new(
image.dimensions(),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
Self::brush_to_shader(
skia_safe::Paint::default(), // Don't use the renderer's default paint because alpha is applied later
colorize_brush,
PhysicalLength::new(image.width() as f32),
PhysicalLength::new(image.height() as f32),
self.scale_factor.get(),
)
.map(|(mut paint, colorize_shader)| {
let mut surface = self.canvas.new_surface(&image_info, None)?;
let canvas = surface.canvas();
canvas.clear(skia_safe::Color::TRANSPARENT);
paint.set_image_filter(skia_safe::image_filters::blend(
skia_safe::BlendMode::SrcIn,
skia_safe::image_filters::image(image, None, None, None),
skia_safe::image_filters::shader(colorize_shader, None),
None,
));
canvas.draw_paint(&paint);
Some(surface.image_snapshot())
})?
}
fn draw_image_impl(
&mut self,
item_rc: &ItemRc,
item: Pin<&dyn RenderImage>,
dest_rect: PhysicalRect,
) {
let tiling = item.tiling();
// TODO: avoid doing creating an SkImage multiple times when the same source is used in multiple image elements
let skia_image = self.image_cache.get_or_update_cache_entry(item_rc, || {
let image = item.source();
super::cached_image::as_skia_image(
image,
&|| item.target_size(),
if tiling != Default::default() { ImageFit::Preserve } else { item.image_fit() },
self.scale_factor,
self.canvas,
self.surface,
)
.and_then(|skia_image| {
let brush = item.colorize();
if !brush.is_transparent() {
self.colorize_image(skia_image, brush)
} else {
Some(skia_image)
}
})
});
let Some(skia_image) = skia_image else { return };
let source = item.source();
let source_size = source.size();
if source_size.is_empty() {
// Not sure how this can happen, but we've seen with #6280
// that somehow we end up with a `skia_safe::Image` but a zero
// source size.
return;
}
let fits = if let i_slint_core::ImageInner::NineSlice(nine) =
<&i_slint_core::ImageInner>::from(&source)
{
i_slint_core::graphics::fit9slice(
source_size.cast(),
nine.1,
dest_rect.size,
self.scale_factor,
item.alignment(),
tiling,
)
.collect::<Vec<_>>()
} else {
vec![i_slint_core::graphics::fit(
item.image_fit(),
dest_rect.size,
item.source_clip().unwrap_or_else(|| euclid::Rect::from_size(source_size.cast())),
self.scale_factor,
item.alignment(),
tiling,
)]
};
let _saved_canvas = self.pixel_align_origin_auto_restore();
for fit in fits {
self.canvas.save();
let dst = to_skia_rect(&PhysicalRect::new(fit.offset, fit.size));
self.canvas.clip_rect(dst, None, None);
let src = skia_safe::IRect::from_xywh(
skia_image.width() * fit.clip_rect.origin.x / source_size.width as i32,
skia_image.height() * fit.clip_rect.origin.y / source_size.height as i32,
skia_image.width() * fit.clip_rect.size.width / source_size.width as i32,
skia_image.height() * fit.clip_rect.size.height / source_size.height as i32,
);
let filter_mode: skia_safe::sampling_options::SamplingOptions =
match item.rendering() {
ImageRendering::Pixelated => skia_safe::sampling_options::FilterMode::Nearest,
ImageRendering::Smooth | _ => skia_safe::sampling_options::FilterMode::Linear,
}
.into();
if let Some(tiled_offset) = fit.tiled {
let matrix = Matrix::translate(((fit.offset.x as i32), (fit.offset.y as i32)))
* Matrix::scale((
fit.source_to_target_x * source_size.width as f32
/ skia_image.width() as f32,
fit.source_to_target_y * source_size.height as f32
/ skia_image.height() as f32,
))
* Matrix::translate((-(tiled_offset.x as i32), -(tiled_offset.y as i32)));
if let Some(shader) = skia_image
.make_subset(
self.canvas
.recording_context()
.as_mut()
.map(|c| c.as_recorder() as &mut dyn skia_safe::Recorder),
src,
skia_safe::image::RequiredProperties::default(),
)
.and_then(|i| {
i.to_shader((TileMode::Repeat, TileMode::Repeat), filter_mode, &matrix)
})
{
let mut paint = self.default_paint().unwrap_or_default();
paint.set_shader(shader);
self.canvas.draw_paint(&paint);
}
} else {
let transform =
skia_safe::Matrix::rect_2_rect(skia_safe::Rect::from(src), dst, None)
.unwrap_or_default();
self.canvas.concat(&transform);
self.canvas.draw_image_with_sampling_options(
skia_image.clone(),
skia_safe::Point::default(),
filter_mode,
self.default_paint().as_ref(),
);
}
self.canvas.restore();
}
}
fn render_and_blend_layer(&mut self, item_rc: &ItemRc) -> RenderingResult {
if let Some((layer_offset, layer_image)) =
i_slint_core::item_rendering::render_layer(self, item_rc)
{
self.canvas.translate(skia_safe::Vector::from((layer_offset.x, layer_offset.y)));
let _saved_canvas = self.pixel_align_origin_auto_restore();
self.canvas.draw_image_with_sampling_options(
layer_image,
skia_safe::Point::default(),
skia_safe::sampling_options::FilterMode::Linear,
self.default_paint().as_ref(),
);
}
RenderingResult::ContinueRenderingWithoutChildren
}
// Same as pixel_align_origin_auto_restore() but can be used across function calls where
// `&self` is needed. Returns true if the caller must call `restore()` on `self.canvas`.
fn save_canvas_and_pixel_align_origin(&self) -> bool {
let local_to_device = self.canvas.local_to_device_as_3x3();
if !keeps_pixel_grid(&local_to_device) || local_to_device.is_identity() {
return false;
}
let Some(device_to_local) = local_to_device.invert() else {
return false;
};
let mut target_point = local_to_device.map_point(skia_safe::Point::default());
target_point.x = target_point.x.round();
target_point.y = target_point.y.round();
self.canvas.save();
self.canvas.translate(device_to_local.map_point(target_point));
true
}
fn pixel_align_origin_auto_restore(&self) -> Option<skia_safe::canvas::AutoRestoredCanvas<'_>> {
let local_to_device = self.canvas.local_to_device_as_3x3();
if !keeps_pixel_grid(&local_to_device) || local_to_device.is_identity() {
return None;
}
let device_to_local = local_to_device.invert()?;
let mut target_point = local_to_device.map_point(skia_safe::Point::default());
target_point.x = target_point.x.round();
target_point.y = target_point.y.round();
let restore_point = skia_safe::AutoCanvasRestore::guard(self.canvas, true);
self.canvas.translate(device_to_local.map_point(target_point));
Some(restore_point)
}
}
/// DarkRoom patch: whether `m` maps whole pixels onto whole pixels, so that
/// rounding the origin in device space aligns everything drawn after it.
///
/// Upstream asks `is_translate()`, which is right until the whole canvas is
/// turned a quarter turn for a pre-rotated swapchain (`wgpu_29_surface`):
/// then no matrix is a pure translation and pixel alignment silently stops
/// everywhere. A right-angle rotation or flip without scaling keeps the
/// grid just as well, so it qualifies too.
fn keeps_pixel_grid(m: &skia_safe::Matrix) -> bool {
let unit = |v: f32| v == 0. || v == 1. || v == -1.;
m.is_translate()
|| (!m.has_perspective()
&& m.rect_stays_rect()
&& unit(m.scale_x())
&& unit(m.scale_y())
&& unit(m.skew_x())
&& unit(m.skew_y()))
}
impl ItemRenderer for SkiaItemRenderer<'_> {
fn draw_rectangle(
&mut self,
rect: Pin<&dyn i_slint_core::item_rendering::RenderRectangle>,
_self_rc: &i_slint_core::items::ItemRc,
size: LogicalSize,
_cache: &CachedRenderingData,
) {
let geometry = PhysicalRect::from(size * self.scale_factor);
if geometry.is_empty() {
return;
}
let paint = match self.brush_to_paint(
rect.background(),
geometry.width_length(),
geometry.height_length(),
) {
Some(paint) => paint,
None => return,
};
self.canvas.draw_rect(to_skia_rect(&geometry), &paint);
}
fn draw_border_rectangle(
&mut self,
rect: Pin<&dyn i_slint_core::item_rendering::RenderBorderRectangle>,
_self_rc: &i_slint_core::items::ItemRc,
size: LogicalSize,
_: &CachedRenderingData,
) {
let mut geometry = PhysicalRect::from(size * self.scale_factor);
if geometry.is_empty() {
return;
}
// Save the original element bounds for gradient positioning. The CSS model positions
// gradients relative to the border box (full element), but adjust_rect_and_border_for_inner_drawing
// shrinks geometry before we create the paint, which would shift the gradient center inward.
let original_width = geometry.width_length();
let original_height = geometry.height_length();
let border_color = rect.border_color();
let opaque_border = border_color.is_opaque();
let mut border_width = if border_color.is_transparent() {
PhysicalLength::new(0.)
} else {
rect.border_width() * self.scale_factor
};
// Radius of rounded rect if we were to just fill the rectangle, without a border.
let mut fill_radius = rect.border_radius() * self.scale_factor;
// Skia's border radius on stroke is in the middle of the border. But we want it to be the radius of the rectangle itself.
// This is incorrect if fill_radius < border_width/2, but this can't be fixed. Better to have a radius a bit too big than no radius at all
fill_radius = fill_radius.outer(border_width / 2. + PhysicalLength::new(0.01));
let stroke_border_radius = fill_radius.inner(border_width / 2.);
let (background_rect, border_rect) = if opaque_border {
// In CSS the border is entirely towards the inside of the boundary
// geometry, while in femtovg the line with for a stroke is 50% in-
// and 50% outwards. We choose the CSS model, so the inner rectangle
// is adjusted accordingly.
adjust_rect_and_border_for_inner_drawing(&mut geometry, &mut border_width);
let rounded_rect = to_skia_rrect(&geometry, &stroke_border_radius);
(rounded_rect, rounded_rect)
} else {
let background_rect = to_skia_rrect(&geometry, &fill_radius);
// In CSS the border is entirely towards the inside of the boundary
// geometry, while in femtovg the line with for a stroke is 50% in-
// and 50% outwards. We choose the CSS model, so the inner rectangle
// is adjusted accordingly.
adjust_rect_and_border_for_inner_drawing(&mut geometry, &mut border_width);
let border_rect = to_skia_rrect(&geometry, &stroke_border_radius);
(background_rect, border_rect)
};
if let Some(mut fill_paint) =
self.brush_to_paint(rect.background(), original_width, original_height)
{
fill_paint.set_style(skia_safe::PaintStyle::Fill);
if !background_rect.is_rect() {
fill_paint.set_anti_alias(true);
}
self.canvas.draw_rrect(background_rect, &fill_paint);
}
if border_width.get() > 0.0
&& let Some(mut border_paint) =
self.brush_to_paint(border_color, original_width, original_height)
{
border_paint.set_style(skia_safe::PaintStyle::Stroke);
border_paint.set_stroke_width(border_width.get());
if !border_rect.is_rect() {
border_paint.set_anti_alias(true);
}
self.canvas.draw_rrect(border_rect, &border_paint);
}
}
fn draw_window_background(
&mut self,
_rect: Pin<&dyn i_slint_core::item_rendering::RenderRectangle>,
_self_rc: &ItemRc,
_size: LogicalSize,
_cache: &CachedRenderingData,
) {
// The background is drawn directly by FemtoVG renderer (via clear_color, if necessary).
}
fn draw_image(
&mut self,
image: Pin<&dyn RenderImage>,
self_rc: &ItemRc,
size: LogicalSize,
_cache: &CachedRenderingData,
) {
let geometry = PhysicalRect::from(size * self.scale_factor);
if geometry.is_empty() {
return;
}
self.draw_image_impl(self_rc, image, geometry);
}
fn draw_text(
&mut self,
text: Pin<&dyn RenderText>,
self_rc: &i_slint_core::items::ItemRc,
size: LogicalSize,
_cache: &CachedRenderingData,
) {
let restore = self.save_canvas_and_pixel_align_origin();
sharedparley::draw_text(self, text, Some(self_rc), size, Some(self.text_layout_cache));
if restore {
self.canvas.restore();
}
}
fn draw_text_input(
&mut self,
text_input: Pin<&i_slint_core::items::TextInput>,
self_rc: &i_slint_core::items::ItemRc,
size: LogicalSize,
) {
let restore = self.save_canvas_and_pixel_align_origin();
sharedparley::draw_text_input(self, text_input, self_rc, size, None);
if restore {
self.canvas.restore();
}
}
fn draw_path(
&mut self,
path: Pin<&i_slint_core::items::Path>,
item_rc: &i_slint_core::items::ItemRc,
size: LogicalSize,
) {
let geometry = PhysicalRect::from(size * self.scale_factor);
let (physical_offset, skpath): (crate::euclid::Vector2D<f32, PhysicalPx>, _) =
match self.path_cache.get_or_update_cache_entry(item_rc, || {
let (logical_offset, path_events): (crate::euclid::Vector2D<f32, LogicalPx>, _) =
path.fitted_path_events(item_rc)?;
let mut builder = skia_safe::PathBuilder::new();
for x in path_events.iter() {
match x {
lyon_path::Event::Begin { at } => {
builder.move_to(to_skia_point(
LogicalPoint::from_untyped(at) * self.scale_factor,
));
}
lyon_path::Event::Line { from: _, to } => {
builder.line_to(to_skia_point(
LogicalPoint::from_untyped(to) * self.scale_factor,
));
}
lyon_path::Event::Quadratic { from: _, ctrl, to } => {
builder.quad_to(
to_skia_point(LogicalPoint::from_untyped(ctrl) * self.scale_factor),
to_skia_point(LogicalPoint::from_untyped(to) * self.scale_factor),
);
}
lyon_path::Event::Cubic { from: _, ctrl1, ctrl2, to } => {
builder.cubic_to(
to_skia_point(
LogicalPoint::from_untyped(ctrl1) * self.scale_factor,
),
to_skia_point(
LogicalPoint::from_untyped(ctrl2) * self.scale_factor,
),
to_skia_point(LogicalPoint::from_untyped(to) * self.scale_factor),
);
}
lyon_path::Event::End { last: _, first: _, close } => {
if close {
builder.close();
}
}
}
}
(logical_offset * self.scale_factor, builder.detach()).into()
}) {
Some(offset_and_path) => offset_and_path,
None => return,
};
self.canvas.translate((physical_offset.x, physical_offset.y));
let anti_alias = path.anti_alias();
// For Path elements with conic gradients, we need to handle the viewbox transformation
let viewbox_width = path.viewbox_width();
let viewbox_height = path.viewbox_height();
let paint = if viewbox_width > 0.0 && viewbox_height > 0.0 {
// If there's a viewbox, we need to create the gradient in viewbox space
// and then transform it to the actual size
let scale_x = geometry.width() / viewbox_width;
let scale_y = geometry.height() / viewbox_height;
let paint = self.default_paint().unwrap_or_default();
if let Some((mut paint, shader)) = Self::brush_to_shader(
paint,
path.fill(),
PhysicalLength::new(viewbox_width),
PhysicalLength::new(viewbox_height),
1.0,
) {
// Apply the viewbox transformation to the shader
let transform = skia_safe::Matrix::scale((scale_x, scale_y));
paint.set_shader(shader.with_local_matrix(&transform));
Some(paint)
} else {
None
}
} else {
self.brush_to_paint(path.fill(), geometry.width_length(), geometry.height_length())
};
if let Some(mut fill_paint) = paint {
fill_paint.set_anti_alias(anti_alias);
self.canvas.draw_path(&skpath, &fill_paint);
}
if let Some(mut border_paint) =
self.brush_to_paint(path.stroke(), geometry.width_length(), geometry.height_length())
{
border_paint.set_anti_alias(anti_alias);
border_paint.set_stroke_width((path.stroke_width() * self.scale_factor).get());
border_paint.set_stroke_cap(match path.stroke_line_cap() {
i_slint_core::items::LineCap::Round => skia_safe::PaintCap::Round,
i_slint_core::items::LineCap::Square => skia_safe::PaintCap::Square,
i_slint_core::items::LineCap::Butt | _ => skia_safe::PaintCap::Butt,
});
border_paint.set_stroke_join(match path.stroke_line_join() {
i_slint_core::items::LineJoin::Round => skia_safe::PaintJoin::Round,
i_slint_core::items::LineJoin::Bevel => skia_safe::PaintJoin::Bevel,
i_slint_core::items::LineJoin::Miter | _ => skia_safe::PaintJoin::Miter,
});
border_paint.set_stroke_miter(path.stroke_miter_limit());
border_paint.set_stroke(true);
self.canvas.draw_path(&skpath, &border_paint);
}
}
fn draw_box_shadow(
&mut self,
box_shadow: Pin<&i_slint_core::items::BoxShadow>,
self_rc: &i_slint_core::items::ItemRc,
_size: LogicalSize,
) {
let offset = LogicalPoint::from_lengths(box_shadow.offset_x(), box_shadow.offset_y())
* self.scale_factor;
let inset = box_shadow.inset();
let spread = box_shadow.spread() * self.scale_factor;
// Drop shadow with no offset / blur / spread is invisible.
if !inset
&& offset.x == 0.
&& offset.y == 0.
&& box_shadow.blur() == LogicalLength::zero()
&& spread == PhysicalLength::zero()
{
return;
}
let cached_shadow_image = self.box_shadow_cache.get_box_shadow(
self_rc,
self.image_cache,
box_shadow,
self.scale_factor,
|shadow_options| {
if shadow_options.inset {
Self::render_inset_shadow_image(self.canvas, shadow_options)
} else {
Self::render_drop_shadow_image(self.canvas, shadow_options)
}
},
);
let cached_shadow_image = match cached_shadow_image {
Some(img) => img,
None => return,
};
if inset {
// Inset image is sized exactly to the geometry; blit at origin.
self.canvas.draw_image(
cached_shadow_image,
skia_safe::Point::new(0., 0.),
self.default_paint().as_ref(),
);
} else {
let blur = box_shadow.blur() * self.scale_factor;
let pad = blur.get() + spread.get().max(0.);
self.canvas.draw_image(
cached_shadow_image,
to_skia_point(offset - PhysicalPoint::new(pad, pad).to_vector()),
self.default_paint().as_ref(),
);
}
}
fn combine_clip(
&mut self,
rect: LogicalRect,
radius: LogicalBorderRadius,
border_width: LogicalLength,
) -> bool {
let mut rect = rect * self.scale_factor;
let mut border_width = border_width * self.scale_factor;
// In CSS the border is entirely towards the inside of the boundary
// geometry, while in femtovg the line with for a stroke is 50% in-
// and 50% outwards. We choose the CSS model, so the inner rectangle
// is adjusted accordingly.
adjust_rect_and_border_for_inner_drawing(&mut rect, &mut border_width);
let radius = radius * self.scale_factor;
let rounded_rect = to_skia_rrect(&rect, &radius);
self.canvas.clip_rrect(rounded_rect, None, true);
self.canvas.local_clip_bounds().is_some()
}
fn get_current_clip(&self) -> LogicalRect {
from_skia_rect(&self.canvas.local_clip_bounds().unwrap_or_default()) / self.scale_factor
}
fn translate(&mut self, distance: LogicalVector) {
self.current_state.transform = self.current_state.transform.pre_translate(distance.cast());
let distance = distance * self.scale_factor;
self.canvas.translate(skia_safe::Vector::from((distance.x, distance.y)));
}
fn current_transform(&self) -> i_slint_core::lengths::ItemTransform {
self.current_state.transform
}
fn rotate(&mut self, angle_in_degrees: f32) {
self.current_state.transform =
self.current_state.transform.pre_rotate(euclid::Angle::degrees(angle_in_degrees));
self.canvas.rotate(angle_in_degrees, None);
}
fn scale(&mut self, x_factor: f32, y_factor: f32) {
self.current_state.transform = self.current_state.transform.pre_scale(x_factor, y_factor);
self.canvas.scale((x_factor, y_factor));
}
fn apply_opacity(&mut self, opacity: f32) {
self.current_state.alpha *= opacity;
}
fn save_state(&mut self) {
self.canvas.save();
self.state_stack.push(self.current_state);
}
fn restore_state(&mut self) {
self.current_state = self.state_stack.pop().unwrap();
self.canvas.restore();
}
fn scale_factor(&self) -> f32 {
self.scale_factor.get()
}
fn draw_cached_pixmap(
&mut self,
item_rc: &i_slint_core::items::ItemRc,
update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
) {
let skia_image = self.image_cache.get_or_update_cache_entry(item_rc, || {
let mut cached_image = None;
update_fn(&mut |width: u32, height: u32, data: &[u8]| {
let image_info = skia_safe::ImageInfo::new(
skia_safe::ISize::new(width as i32, height as i32),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
cached_image = skia_safe::images::raster_from_data(
&image_info,
skia_safe::Data::new_copy(data),
width as usize * 4,
);
});
cached_image
});
let skia_image = match skia_image {
Some(img) => img,
None => return,
};
let _saved_canvas = self.pixel_align_origin_auto_restore();
self.canvas.draw_image(skia_image, skia_safe::Point::default(), None);
}
fn draw_string(&mut self, string: &str, color: i_slint_core::Color) {
sharedparley::draw_text(
self,
std::pin::pin!((SharedString::from(string), Brush::from(color))),
None,
logical_size_from_api(self.window.size().to_logical(self.scale_factor())),
None,
);
}
fn draw_image_direct(&mut self, image: i_slint_core::graphics::Image) {
let skia_image = super::cached_image::as_skia_image(
image.clone(),
&|| LogicalSize::from_untyped(image.size().cast()),
ImageFit::Fill,
self.scale_factor,
self.canvas,
self.surface,
);
let skia_image = match skia_image {
Some(img) => img,
None => return,
};
self.canvas.draw_image(
skia_image,
skia_safe::Point::default(),
self.default_paint().as_ref(),
);
}
fn window(&self) -> &i_slint_core::window::WindowInner {
i_slint_core::window::WindowInner::from_pub(self.window)
}
fn as_any(&mut self) -> Option<&mut dyn core::any::Any> {
None
}
fn visit_opacity(
&mut self,
opacity_item: Pin<&Opacity>,
item_rc: &ItemRc,
_size: LogicalSize,
) -> RenderingResult {
let opacity = opacity_item.opacity();
if Opacity::need_layer(item_rc, opacity) {
self.canvas.save_layer_alpha(None, (opacity * 255.) as u32);
self.state_stack.push(self.current_state);
self.current_state.alpha = 1.0;
let window_adapter = WindowInner::from_pub(self.window).window_adapter();
i_slint_core::item_rendering::render_item_children(
self,
item_rc.item_tree(),
item_rc.index() as isize,
&window_adapter,
);
self.current_state = self.state_stack.pop().unwrap();
self.canvas.restore();
RenderingResult::ContinueRenderingWithoutChildren
} else {
self.apply_opacity(opacity);
RenderingResult::ContinueRenderingChildren
}
}
fn visit_layer(
&mut self,
layer_item: Pin<&Layer>,
self_rc: &ItemRc,
_size: LogicalSize,
) -> RenderingResult {
if layer_item.cache_rendering_hint() {
self.render_and_blend_layer(self_rc)
} else {
self.image_cache.release(self_rc);
RenderingResult::ContinueRenderingChildren
}
}
}
impl<'a> LayerRenderer<'a> for SkiaItemRenderer<'a> {
type LayerTarget = skia_safe::Surface;
type Image = skia_safe::Image;
fn layer_cache(&self) -> &'a ItemCache<Option<(PhysicalPoint, Self::Image)>> {
self.layer_cache
}
fn create_layer_target(
&mut self,
_item_rc: &ItemRc,
physical_size: euclid::Size2D<f32, PhysicalPx>,
) -> Option<Self::LayerTarget> {
let image_info = skia_safe::ImageInfo::new(
to_skia_size(&physical_size).to_ceil(),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
self.canvas.new_surface(&image_info, None)
}
fn render_into_layer(
&mut self,
mut surface: Self::LayerTarget,
item_rc: &ItemRc,
bounding_rect: LogicalRect,
) -> Self::Image {
let canvas = surface.canvas();
canvas.clear(skia_safe::Color::TRANSPARENT);
let mut sub_renderer = SkiaItemRenderer::new(
canvas,
self.window,
self.surface,
self.image_cache,
self.layer_cache,
self.path_cache,
self.text_layout_cache,
self.box_shadow_cache,
);
sub_renderer.translate(-bounding_rect.origin.to_vector());
i_slint_core::item_rendering::render_item_children(
&mut sub_renderer,
item_rc.item_tree(),
item_rc.index() as isize,
&WindowInner::from_pub(self.window).window_adapter(),
);
surface.image_snapshot()
}
}
impl GlyphRenderer for SkiaItemRenderer<'_> {
type PlatformBrush = skia_safe::Paint;
fn platform_text_fill_brush(
&mut self,
brush: i_slint_core::Brush,
size: LogicalSize,
) -> Option<Self::PlatformBrush> {
self.brush_to_paint(
brush,
size.width_length() * self.scale_factor,
size.height_length() * self.scale_factor,
)
}
fn platform_brush_for_color(
&mut self,
color: &i_slint_core::Color,
) -> Option<Self::PlatformBrush> {
if color.alpha() == 0 {
None
} else {
let mut paint = self.default_paint().unwrap_or_default();
paint.set_shader(skia_safe::shaders::color(to_skia_color(color)));
Some(paint)
}
}
fn platform_text_stroke_brush(
&mut self,
brush: i_slint_core::Brush,
physical_stroke_width: f32,
size: LogicalSize,
) -> Option<Self::PlatformBrush> {
match self.brush_to_paint(
brush.clone(),
size.width_length() * self.scale_factor,
size.height_length() * self.scale_factor,
) {
Some(mut stroke_paint) => {
stroke_paint.set_style(skia_safe::PaintStyle::Stroke);
stroke_paint.set_stroke_width(physical_stroke_width);
// Set stroke cap/join/miter to match FemtoVG
stroke_paint.set_stroke_cap(skia_safe::PaintCap::Butt);
stroke_paint.set_stroke_join(skia_safe::PaintJoin::Miter);
stroke_paint.set_stroke_miter(10.0);
Some(stroke_paint)
}
None => None,
}
}
fn draw_glyph_run(
&mut self,
font: &sharedparley::parley::FontData,
font_size: PhysicalLength,
_normalized_coords: &[i16],
synthesis: &fontique::Synthesis,
brush: Self::PlatformBrush,
y_offset: sharedparley::PhysicalLength,
glyphs_it: &mut dyn Iterator<Item = sharedparley::parley::layout::Glyph>,
) {
let Some(type_face) = crate::font_cache::FONT_CACHE
.with_borrow_mut(|font_cache| font_cache.font_with_variations(font, synthesis))
else {
return;
};
let mut font = skia_safe::Font::from_typeface(type_face, font_size.get());
font.set_subpixel(true);
let (glyph_ids, glyph_positions): (Vec<_>, Vec<_>) = glyphs_it
.into_iter()
.map(|g| (g.id as skia_safe::GlyphId, skia_safe::Point::new(g.x, g.y + y_offset.get())))
.unzip();
self.canvas.draw_glyphs_at(
&glyph_ids,
skia_safe::canvas::GlyphPositions::Points(&glyph_positions),
skia_safe::Point::default(),
&font,
&brush,
);
}
fn fill_rectangle(
&mut self,
physical_rect: sharedparley::PhysicalRect,
paint: Self::PlatformBrush,
) {
self.canvas.draw_rect(
skia_safe::Rect::from_xywh(
physical_rect.min_x(),
physical_rect.min_y(),
physical_rect.width(),
physical_rect.height(),
),
&paint,
);
}
}
pub fn from_skia_rect(rect: &skia_safe::Rect) -> PhysicalRect {
let top_left = euclid::Point2D::new(rect.left, rect.top);
let bottom_right = euclid::Point2D::new(rect.right, rect.bottom);
euclid::Box2D::new(top_left, bottom_right).to_rect()
}
pub fn to_skia_rect(rect: &PhysicalRect) -> skia_safe::Rect {
skia_safe::Rect::from_xywh(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)
}
pub fn to_skia_rrect(rect: &PhysicalRect, radius: &PhysicalBorderRadius) -> skia_safe::RRect {
if let Some(radius) = radius.as_uniform() {
skia_safe::RRect::new_rect_xy(to_skia_rect(rect), radius, radius)
} else {
skia_safe::RRect::new_rect_radii(
to_skia_rect(rect),
&[
skia_safe::Point::new(radius.top_left, radius.top_left),
skia_safe::Point::new(radius.top_right, radius.top_right),
skia_safe::Point::new(radius.bottom_right, radius.bottom_right),
skia_safe::Point::new(radius.bottom_left, radius.bottom_left),
],
)
}
}
impl ItemRendererFeatures for SkiaItemRenderer<'_> {
const SUPPORTS_TRANSFORMATIONS: bool = true;
}
pub fn to_skia_point(point: PhysicalPoint) -> skia_safe::Point {
skia_safe::Point::new(point.x, point.y)
}
pub fn to_skia_size(size: &PhysicalSize) -> skia_safe::Size {
skia_safe::Size::new(size.width, size.height)
}
pub fn to_skia_color(col: &Color) -> skia_safe::Color {
skia_safe::Color::from_argb(col.alpha(), col.red(), col.green(), col.blue())
}
fn adjust_rect_and_border_for_inner_drawing(
rect: &mut PhysicalRect,
border_width: &mut PhysicalLength,
) {
// If the border width exceeds the width, just fill the rectangle.
*border_width = border_width.min(rect.width_length() / 2.);
// adjust the size so that the border is drawn within the geometry
rect.origin += PhysicalSize::from_lengths(*border_width / 2., *border_width / 2.);
rect.size -= PhysicalSize::from_lengths(*border_width, *border_width);
}