363 lines
11 KiB
Rust

use skia_safe::{self as skia, Paint, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::Result;
use crate::get_resources;
use crate::render::get_source_rect;
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
// Set the clipping area to the shape outline within the container bounds
fn clip_to_shape(
canvas: &skia::Canvas,
shape: &Shape,
container: &crate::math::Rect,
antialias: bool,
) {
match &shape.shape_type {
Type::Rect(Rect {
corners: Some(corners),
})
| Type::Frame(Frame {
corners: Some(corners),
..
}) => {
let rrect: RRect = RRect::new_rect_radii(container, corners);
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
}
Type::Rect(_) | Type::Frame(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
}
Type::Circle => {
let oval_path = {
let mut pb = skia::PathBuilder::new();
pb.add_oval(container, None, None);
pb.detach()
};
canvas.clip_path(&oval_path, skia::ClipOp::Intersect, antialias);
}
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() {
if let Some(path_transform) = shape.to_path_transform() {
canvas.clip_path(
&path
.to_skia_path(shape.svg_attrs.as_ref())
.make_transform(&path_transform),
skia::ClipOp::Intersect,
antialias,
);
}
}
}
Type::SVGRaw(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
}
Type::Group(_) => unreachable!("A group should not have fills"),
Type::Text(_) => unimplemented!("TODO"),
}
}
fn draw_image_fill(
render_state: &mut RenderState,
shape: &Shape,
image_fill: &ImageFill,
paint: &Paint,
antialias: bool,
surface_id: SurfaceId,
) {
if draw_svg_image_fill(
render_state,
shape,
image_fill,
paint,
antialias,
surface_id,
) {
return;
}
let Some(image) = get_resources().images.get(&image_fill.id()) else {
return;
};
let size = image.dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let src_rect = get_source_rect(size, container, image_fill);
let dest_rect = container;
let mut image_paint = skia::Paint::default();
image_paint.set_anti_alias(antialias);
if let Some(filter) = shape.image_filter(1.) {
image_paint.set_image_filter(filter.clone());
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
// Save the current canvas state
canvas.save_layer(&layer_rec);
clip_to_shape(canvas, shape, container, antialias);
// Draw the image with the calculated destination rectangle
canvas.draw_image_rect_with_sampling_options(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
get_resources().sampling_options,
paint,
);
// Restore the canvas to remove the clipping
canvas.restore();
}
// Draws an SVG image fill from its parsed DOM. The canvas transform carries
// the current zoom, so the SVG rasterizes at display resolution instead of
// being stretched from a fixed-size texture. Returns false when the stored
// image is not an SVG.
fn draw_svg_image_fill(
render_state: &mut RenderState,
shape: &Shape,
image_fill: &ImageFill,
paint: &Paint,
antialias: bool,
surface_id: SurfaceId,
) -> bool {
let Some((dom, size)) = get_resources().images.get_svg(&image_fill.id()) else {
return false;
};
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let size = skia::ISize::new(size.width as i32, size.height as i32);
let src_rect = get_source_rect(size, container, image_fill);
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
return true;
}
let mut image_paint = skia::Paint::default();
image_paint.set_anti_alias(antialias);
if let Some(filter) = shape.image_filter(1.) {
image_paint.set_image_filter(filter.clone());
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
canvas.save_layer(&layer_rec);
clip_to_shape(canvas, shape, container, antialias);
// Apply the fill paint (opacity/blend) to the whole SVG as one layer.
let fill_layer = skia::canvas::SaveLayerRec::default().paint(paint);
canvas.save_layer(&fill_layer);
// Map the cropped source rect onto the container: cover semantics when
// keep-aspect-ratio is set, stretch otherwise (same math as the raster
// path, expressed as a canvas transform).
let scale_x = container.width() / src_rect.width();
let scale_y = container.height() / src_rect.height();
canvas.translate((
container.left - src_rect.left * scale_x,
container.top - src_rect.top * scale_y,
));
canvas.scale((scale_x, scale_y));
dom.render(canvas);
canvas.restore();
canvas.restore();
true
}
/**
* This SHOULD be the only public function in this module.
*/
pub fn render(
render_state: &mut RenderState,
shape: &Shape,
fills: &[Fill],
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
) -> Result<()> {
if fills.is_empty() {
return Ok(());
}
let scale = render_state.get_scale().max(1e-6);
let inset = if shape.has_inner_stroke() {
Some(1.0 / scale)
} else {
None
};
// Image fills use draw_image_fill which needs render_state for GPU images
// and sampling options that get_fill_shader (used by merge_fills) lacks.
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
if has_image_fills {
for fill in fills.iter().rev() {
render_single_fill(
render_state,
shape,
fill,
antialias,
surface_id,
outset,
inset,
)?;
}
return Ok(());
}
let mut paint = merge_fills(fills, shape.selrect);
paint.set_anti_alias(antialias);
if let Some(image_filter) = shape.image_filter(1.) {
let bounds = image_filter.compute_fast_bounds(shape.selrect);
if filters::render_with_filter_surface(
render_state,
bounds,
surface_id,
|state, temp_surface| {
let mut filtered_paint = paint.clone();
filtered_paint.set_image_filter(image_filter.clone());
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset);
Ok(())
},
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
}
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
Ok(())
}
/// Draws a single paint (with a merged shader) to the appropriate surface
/// based on the shape type.
/// When `inset` is Some(eps), the fill is inset by eps (e.g. to avoid seam with inner strokes).
fn draw_fill_to_surface(
render_state: &mut RenderState,
shape: &Shape,
surface_id: SurfaceId,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
match &shape.shape_type {
Type::Rect(_) | Type::Frame(_) => {
render_state
.surfaces
.draw_rect_to(surface_id, shape, paint, outset, inset);
}
Type::Circle => {
render_state
.surfaces
.draw_circle_to(surface_id, shape, paint, outset, inset);
}
Type::Path(_) | Type::Bool(_) => {
render_state
.surfaces
.draw_path_to(surface_id, shape, paint, outset, inset);
}
Type::Group(_) => {}
_ => unreachable!("This shape should not have fills"),
}
}
fn render_single_fill(
render_state: &mut RenderState,
shape: &Shape,
fill: &Fill,
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
inset: Option<f32>,
) -> Result<()> {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
let bounds = image_filter.compute_fast_bounds(shape.selrect);
if filters::render_with_filter_surface(
render_state,
bounds,
surface_id,
|state, temp_surface| {
let mut filtered_paint = paint.clone();
filtered_paint.set_image_filter(image_filter.clone());
draw_single_fill_to_surface(
state,
shape,
fill,
antialias,
temp_surface,
&filtered_paint,
outset,
inset,
);
Ok(())
},
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
}
draw_single_fill_to_surface(
render_state,
shape,
fill,
antialias,
surface_id,
&paint,
outset,
inset,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_single_fill_to_surface(
render_state: &mut RenderState,
shape: &Shape,
fill: &Fill,
antialias: bool,
surface_id: SurfaceId,
paint: &Paint,
outset: Option<f32>,
inset: Option<f32>,
) {
match (fill, &shape.shape_type) {
(Fill::Image(image_fill), _) => {
draw_image_fill(
render_state,
shape,
image_fill,
paint,
antialias,
surface_id,
);
}
(_, Type::Rect(_) | Type::Frame(_)) => {
render_state
.surfaces
.draw_rect_to(surface_id, shape, paint, outset, inset);
}
(_, Type::Circle) => {
render_state
.surfaces
.draw_circle_to(surface_id, shape, paint, outset, inset);
}
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
render_state
.surfaces
.draw_path_to(surface_id, shape, paint, outset, inset);
}
(_, Type::Group(_)) => {
// Groups can have fills but they propagate them to their children
}
_ => unreachable!("This shape should not have fills"),
}
}