use crate::core::renderer::Quad;
use crate::core::{
Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
};
use crate::graphics::{Image, Text};
use crate::text;
use crate::Primitive;
#[derive(Debug)]
pub struct Engine {
text_pipeline: text::Pipeline,
#[cfg(feature = "image")]
pub(crate) raster_pipeline: crate::raster::Pipeline,
#[cfg(feature = "svg")]
pub(crate) vector_pipeline: crate::vector::Pipeline,
}
impl Engine {
pub fn new() -> Self {
Self {
text_pipeline: text::Pipeline::new(),
#[cfg(feature = "image")]
raster_pipeline: crate::raster::Pipeline::new(),
#[cfg(feature = "svg")]
vector_pipeline: crate::vector::Pipeline::new(),
}
}
pub fn draw_quad(
&mut self,
quad: &Quad,
background: &Background,
transformation: Transformation,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: &mut tiny_skia::Mask,
clip_bounds: Rectangle,
) {
debug_assert!(
quad.bounds.width.is_normal(),
"Quad with non-normal width!"
);
debug_assert!(
quad.bounds.height.is_normal(),
"Quad with non-normal height!"
);
let physical_bounds = quad.bounds * transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds.is_within_strict(&clip_bounds))
.then_some(clip_mask as &_);
let transform = into_transform(transformation);
let border_width = quad
.border
.width
.min(quad.bounds.width / 2.0)
.min(quad.bounds.height / 2.0);
let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius);
let path_bounds = Rectangle {
x: quad.bounds.x + border_width,
y: quad.bounds.y + border_width,
width: quad.bounds.width - 2.0 * border_width,
height: quad.bounds.height - 2.0 * border_width,
};
for radius in &mut fill_border_radius {
*radius = (*radius - border_width / 2.0)
.min(path_bounds.width / 2.0)
.min(path_bounds.height / 2.0);
}
let path = rounded_rectangle(path_bounds, fill_border_radius);
let shadow = quad.shadow;
if false {
let shadow_bounds = Rectangle {
x: quad.bounds.x + shadow.offset.x - shadow.blur_radius,
y: quad.bounds.y + shadow.offset.y - shadow.blur_radius,
width: quad.bounds.width + shadow.blur_radius * 2.0,
height: quad.bounds.height + shadow.blur_radius * 2.0,
} * transformation;
let radii = fill_border_radius
.into_iter()
.map(|radius| radius * transformation.scale_factor())
.collect::<Vec<_>>();
let (x, y, width, height) = (
shadow_bounds.x as u32,
shadow_bounds.y as u32,
shadow_bounds.width as u32,
shadow_bounds.height as u32,
);
let half_width = physical_bounds.width / 2.0;
let half_height = physical_bounds.height / 2.0;
let colors = (y..y + height)
.flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32)))
.filter_map(|(x, y)| {
tiny_skia::Size::from_wh(half_width, half_height).map(
|size| {
let shadow_distance = rounded_box_sdf(
Vector::new(
x - physical_bounds.position().x
- (shadow.offset.x
* transformation.scale_factor())
- half_width,
y - physical_bounds.position().y
- (shadow.offset.y
* transformation.scale_factor())
- half_height,
),
size,
&radii,
)
.max(0.0);
let shadow_alpha = 1.0
- smoothstep(
-shadow.blur_radius
* transformation.scale_factor(),
shadow.blur_radius
* transformation.scale_factor(),
shadow_distance,
);
let mut color = into_color(shadow.color);
color.apply_opacity(shadow_alpha);
color.to_color_u8().premultiply()
},
)
})
.collect();
if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height)
.and_then(|size| {
tiny_skia::Pixmap::from_vec(
bytemuck::cast_vec(colors),
size,
)
})
{
pixels.draw_pixmap(
x as i32,
y as i32,
pixmap.as_ref(),
&tiny_skia::PixmapPaint::default(),
tiny_skia::Transform::default(),
None,
);
}
}
pixels.fill_path(
&path,
&tiny_skia::Paint {
shader: match background {
Background::Color(color) => {
tiny_skia::Shader::SolidColor(into_color(*color))
}
Background::Gradient(Gradient::Linear(linear)) => {
let (start, end) =
linear.angle.to_distance(&quad.bounds);
let stops: Vec<tiny_skia::GradientStop> = linear
.stops
.into_iter()
.flatten()
.map(|stop| {
tiny_skia::GradientStop::new(
stop.offset,
tiny_skia::Color::from_rgba(
stop.color.b,
stop.color.g,
stop.color.r,
stop.color.a,
)
.expect("Create color"),
)
})
.collect();
tiny_skia::LinearGradient::new(
tiny_skia::Point {
x: start.x,
y: start.y,
},
tiny_skia::Point { x: end.x, y: end.y },
if stops.is_empty() {
vec![tiny_skia::GradientStop::new(
0.0,
tiny_skia::Color::BLACK,
)]
} else {
stops
},
tiny_skia::SpreadMode::Pad,
tiny_skia::Transform::identity(),
)
.expect("Create linear gradient")
}
},
anti_alias: true,
..tiny_skia::Paint::default()
},
tiny_skia::FillRule::EvenOdd,
transform,
clip_mask,
);
if border_width > 0.0 {
let border_bounds = Rectangle {
x: quad.bounds.x + border_width / 2.0,
y: quad.bounds.y + border_width / 2.0,
width: quad.bounds.width - border_width,
height: quad.bounds.height - border_width,
};
let mut border_radius = <[f32; 4]>::from(quad.border.radius);
let mut is_simple_border = true;
for radius in &mut border_radius {
*radius = if *radius == 0.0 {
0.0
} else if *radius > border_width / 2.0 {
*radius - border_width / 2.0
} else {
is_simple_border = false;
0.0
}
.min(border_bounds.width / 2.0)
.min(border_bounds.height / 2.0);
}
if is_simple_border {
let border_path =
rounded_rectangle(border_bounds, border_radius);
pixels.stroke_path(
&border_path,
&tiny_skia::Paint {
shader: tiny_skia::Shader::SolidColor(into_color(
quad.border.color,
)),
anti_alias: true,
..tiny_skia::Paint::default()
},
&tiny_skia::Stroke {
width: border_width,
..tiny_skia::Stroke::default()
},
transform,
clip_mask,
);
} else {
let mut temp_pixmap = tiny_skia::Pixmap::new(
path_bounds.width as u32,
path_bounds.height as u32,
)
.unwrap();
let mut quad_mask = tiny_skia::Mask::new(
path_bounds.width as u32,
path_bounds.height as u32,
)
.unwrap();
let zero_bounds = Rectangle {
x: 0.0,
y: 0.0,
width: path_bounds.width,
height: path_bounds.height,
};
let path = rounded_rectangle(zero_bounds, fill_border_radius);
quad_mask.fill_path(
&path,
tiny_skia::FillRule::EvenOdd,
true,
transform,
);
let path_bounds = Rectangle {
x: (border_width / 2.0),
y: (border_width / 2.0),
width: path_bounds.width - border_width,
height: path_bounds.height - border_width,
};
let border_radius_path =
rounded_rectangle(path_bounds, border_radius);
temp_pixmap.stroke_path(
&border_radius_path,
&tiny_skia::Paint {
shader: tiny_skia::Shader::SolidColor(into_color(
quad.border.color,
)),
anti_alias: true,
..tiny_skia::Paint::default()
},
&tiny_skia::Stroke {
width: border_width,
..tiny_skia::Stroke::default()
},
transform,
Some(&quad_mask),
);
pixels.draw_pixmap(
(quad.bounds.x) as i32,
(quad.bounds.y) as i32,
temp_pixmap.as_ref(),
&tiny_skia::PixmapPaint::default(),
transform,
clip_mask,
);
}
}
}
pub fn draw_text(
&mut self,
text: &Text,
transformation: Transformation,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: &mut tiny_skia::Mask,
clip_bounds: Rectangle,
) {
match text {
Text::Paragraph {
paragraph,
position,
color,
clip_bounds: _, transformation: local_transformation,
} => {
let transformation = transformation * *local_transformation;
let physical_bounds =
Rectangle::new(*position, paragraph.min_bounds)
* transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds
.is_within_strict(&clip_bounds))
.then_some(clip_mask as &_);
self.text_pipeline.draw_paragraph(
paragraph,
*position,
*color,
pixels,
clip_mask,
transformation,
);
}
Text::Editor {
editor,
position,
color,
clip_bounds: _, transformation: local_transformation,
} => {
let transformation = transformation * *local_transformation;
let physical_bounds =
Rectangle::new(*position, editor.bounds) * transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds
.is_within_strict(&clip_bounds))
.then_some(clip_mask as &_);
self.text_pipeline.draw_editor(
editor,
*position,
*color,
pixels,
clip_mask,
transformation,
);
}
Text::Cached {
content,
bounds,
color,
size,
line_height,
font,
horizontal_alignment,
vertical_alignment,
shaping,
clip_bounds: text_bounds, } => {
let physical_bounds = *text_bounds * transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds
.is_within_strict(&clip_bounds))
.then_some(clip_mask as &_);
self.text_pipeline.draw_cached(
content,
*bounds,
*color,
*size,
*line_height,
*font,
*horizontal_alignment,
*vertical_alignment,
*shaping,
pixels,
clip_mask,
transformation,
);
}
Text::Raw {
raw,
transformation: local_transformation,
} => {
let Some(buffer) = raw.buffer.upgrade() else {
return;
};
let transformation = transformation * *local_transformation;
let (width_opt, height_opt) = buffer.size();
let physical_bounds = Rectangle::new(
raw.position,
Size::new(
width_opt.unwrap_or(clip_bounds.width),
height_opt.unwrap_or(clip_bounds.height),
),
) * transformation;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds.is_within(&clip_bounds))
.then_some(clip_mask as &_);
self.text_pipeline.draw_raw(
&buffer,
raw.position,
raw.color,
pixels,
clip_mask,
transformation,
);
}
}
}
pub fn draw_primitive(
&mut self,
primitive: &Primitive,
transformation: Transformation,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: &mut tiny_skia::Mask,
layer_bounds: Rectangle,
) {
match primitive {
Primitive::Fill { path, paint, rule } => {
let physical_bounds = {
let bounds = path.bounds();
Rectangle {
x: bounds.x(),
y: bounds.y(),
width: bounds.width(),
height: bounds.height(),
} * transformation
};
let Some(clip_bounds) =
layer_bounds.intersection(&physical_bounds)
else {
return;
};
let clip_mask =
(physical_bounds != clip_bounds).then_some(clip_mask as &_);
pixels.fill_path(
path,
paint,
*rule,
into_transform(transformation),
clip_mask,
);
}
Primitive::Stroke {
path,
paint,
stroke,
} => {
let physical_bounds = {
let bounds = path.bounds();
Rectangle {
x: bounds.x(),
y: bounds.y(),
width: bounds.width(),
height: bounds.height(),
} * transformation
};
let Some(clip_bounds) =
layer_bounds.intersection(&physical_bounds)
else {
return;
};
let clip_mask =
(physical_bounds != clip_bounds).then_some(clip_mask as &_);
pixels.stroke_path(
path,
paint,
stroke,
into_transform(transformation),
clip_mask,
);
}
}
}
pub fn draw_image(
&mut self,
image: &Image,
_transformation: Transformation,
_pixels: &mut tiny_skia::PixmapMut<'_>,
_clip_mask: &mut tiny_skia::Mask,
_clip_bounds: Rectangle,
) {
match image {
#[cfg(feature = "image")]
Image::Raster { handle, bounds } => {
use tiny_skia::Transform;
let physical_bounds = *bounds * _transformation;
if !_clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
.then_some(_clip_mask as &_);
let center = physical_bounds.center();
let radians = f32::from(handle.rotation);
let transform = Transform::default().post_rotate_at(
radians.to_degrees(),
center.x,
center.y,
);
self.raster_pipeline.draw(
&handle.handle,
handle.filter_method,
physical_bounds,
handle.opacity,
_pixels,
transform,
clip_mask,
handle.border_radius,
);
}
#[cfg(feature = "svg")]
Image::Vector { handle, bounds } => {
let physical_bounds = *bounds * _transformation;
if !_clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
.then_some(_clip_mask as &_);
let center = physical_bounds.center();
let radians = f32::from(handle.rotation);
let transform = tiny_skia::Transform::default().post_rotate_at(
radians.to_degrees(),
center.x,
center.y,
);
self.vector_pipeline.draw(
&handle.handle,
handle.color,
physical_bounds,
handle.opacity,
_pixels,
transform,
clip_mask,
);
}
#[cfg(not(feature = "image"))]
Image::Raster { .. } => {
log::warn!(
"Unsupported primitive in `iced_tiny_skia`: {image:?}",
);
}
#[cfg(not(feature = "svg"))]
Image::Vector { .. } => {
log::warn!(
"Unsupported primitive in `iced_tiny_skia`: {image:?}",
);
}
}
}
pub fn trim(&mut self) {
self.text_pipeline.trim_cache();
#[cfg(feature = "image")]
self.raster_pipeline.trim_cache();
#[cfg(feature = "svg")]
self.vector_pipeline.trim_cache();
}
}
pub fn into_color(color: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
.expect("Convert color from iced to tiny_skia")
}
fn into_transform(transformation: Transformation) -> tiny_skia::Transform {
let translation = transformation.translation();
tiny_skia::Transform {
sx: transformation.scale_factor(),
kx: 0.0,
ky: 0.0,
sy: transformation.scale_factor(),
tx: translation.x,
ty: translation.y,
}
}
fn rounded_rectangle(
bounds: Rectangle,
border_radius: [f32; 4],
) -> tiny_skia::Path {
let [top_left, top_right, bottom_right, bottom_left] = border_radius;
if top_left == 0.0
&& top_right == 0.0
&& bottom_right == 0.0
&& bottom_left == 0.0
{
return tiny_skia::PathBuilder::from_rect(
tiny_skia::Rect::from_xywh(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
)
.expect("Build quad rectangle"),
);
}
if top_left == top_right
&& top_left == bottom_right
&& top_left == bottom_left
&& top_left == bounds.width / 2.0
&& top_left == bounds.height / 2.0
{
return tiny_skia::PathBuilder::from_circle(
bounds.x + bounds.width / 2.0,
bounds.y + bounds.height / 2.0,
top_left,
)
.expect("Build circle path");
}
let mut builder = tiny_skia::PathBuilder::new();
builder.move_to(bounds.x + top_left, bounds.y);
builder.line_to(bounds.x + bounds.width - top_right, bounds.y);
if top_right > 0.0 {
arc_to(
&mut builder,
bounds.x + bounds.width - top_right,
bounds.y,
bounds.x + bounds.width,
bounds.y + top_right,
top_right,
);
}
maybe_line_to(
&mut builder,
bounds.x + bounds.width,
bounds.y + bounds.height - bottom_right,
);
if bottom_right > 0.0 {
arc_to(
&mut builder,
bounds.x + bounds.width,
bounds.y + bounds.height - bottom_right,
bounds.x + bounds.width - bottom_right,
bounds.y + bounds.height,
bottom_right,
);
}
maybe_line_to(
&mut builder,
bounds.x + bottom_left,
bounds.y + bounds.height,
);
if bottom_left > 0.0 {
arc_to(
&mut builder,
bounds.x + bottom_left,
bounds.y + bounds.height,
bounds.x,
bounds.y + bounds.height - bottom_left,
bottom_left,
);
}
maybe_line_to(&mut builder, bounds.x, bounds.y + top_left);
if top_left > 0.0 {
arc_to(
&mut builder,
bounds.x,
bounds.y + top_left,
bounds.x + top_left,
bounds.y,
top_left,
);
}
builder.finish().expect("Build rounded rectangle path")
}
fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) {
if path.last_point() != Some(tiny_skia::Point { x, y }) {
path.line_to(x, y);
}
}
fn arc_to(
path: &mut tiny_skia::PathBuilder,
x_from: f32,
y_from: f32,
x_to: f32,
y_to: f32,
radius: f32,
) {
let svg_arc = kurbo::SvgArc {
from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)),
to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)),
radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)),
x_rotation: 0.0,
large_arc: false,
sweep: true,
};
match kurbo::Arc::from_svg_arc(&svg_arc) {
Some(arc) => {
arc.to_cubic_beziers(0.1, |p1, p2, p| {
path.cubic_to(
p1.x as f32,
p1.y as f32,
p2.x as f32,
p2.y as f32,
p.x as f32,
p.y as f32,
);
});
}
None => {
path.line_to(x_to, y_to);
}
}
}
fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
let x = ((x - a) / (b - a)).clamp(0.0, 1.0);
x * x * (3.0 - 2.0 * x)
}
fn rounded_box_sdf(
to_center: Vector,
size: tiny_skia::Size,
radii: &[f32],
) -> f32 {
let radius = match (to_center.x > 0.0, to_center.y > 0.0) {
(true, true) => radii[2],
(true, false) => radii[1],
(false, true) => radii[3],
(false, false) => radii[0],
};
let x = (to_center.x.abs() - size.width() + radius).max(0.0);
let y = (to_center.y.abs() - size.height() + radius).max(0.0);
(x.powf(2.0) + y.powf(2.0)).sqrt() - radius
}
pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) {
clip_mask.clear();
let path = {
let mut builder = tiny_skia::PathBuilder::new();
builder.push_rect(
tiny_skia::Rect::from_xywh(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
)
.unwrap(),
);
builder.finish().unwrap()
};
clip_mask.fill_path(
&path,
tiny_skia::FillRule::EvenOdd,
false,
tiny_skia::Transform::default(),
);
}