summaryrefslogblamecommitdiffstats
path: root/tiny_skia/src/backend.rs
blob: b6487b38869fa92ca7dcc3bd447b5d16770711fd (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                  
                                                                         
  
                             
                          
                                        
                                        



                     
                                         


                                             


                                             


              
                          
              
                                                        


                                                            


                                                            

         


                               
                                              
                                        

                                 
                             
                                
                      
       
                                                     
                                                          
 
                                























                                                                            
         
 
                               



                                                         

                                  



                                                   
                             
                      



                                                                     
                                      
                                                             





                                                 
 
                                                
 






                                         
                                             


                  

















                                                                         
         
 

                                        


                                          

                                          
     
 



                                              
                                        
                               
                          
                                       

                         


                             
                       
                       
                  








                                                  
                                                                                

                                                              


                           
                                                                          
                                                
 

                                                              
 
                                                                            

                                         


                                              
                                                                             
                                                       


                                                  
                 
                                                                          
 





                                                                           
                                       



















                                                                             














                                                                             
                                      











                                                                               









                                                                      
                         








                                                               


                     
                                 

                                       





                                                                         

                                                                               
                                                                     





































                                                                            
                             
                          


                                                     
                                                 
                              
                              
                  
 




                                                                     



                                                             
                                                             
                                                                            
























                                                                            
                                                             













                                                                                                    
                                                                     




                                                 
                                                                 

































                                                                               
                                                             















                                                              
                                                               



                                      
                 
             



                                  
                                                                 
                  
                                     


                                                                   









                                                                          
                              
                           
                                 

                              
                                   

                  



                               
                                                                 
                  


                                                                              









                                                                          
                              



                                 
                                   

                  




                             
                            


                                     
                        
                                                                 
                  
                                     
                                                                       

                                                              


                           
                                                                          
                                                
 
                                               
                            
                            
                           

                                 


                                          
                             
                                 
                           
                              
                                   

                  



                                          
                                                                 




                                                          

                                                    
                                     


                                                                       









                                                                          
                              



                                 
                                   

                  
                                     




                              
                                                                                

                                                              


                           


                                                                          

                                                              
 







                                          
             


                                          
                                                                               

                  



                                   
                      
                  
                                                                                







                                                                          

                                          
                           
                                                              
                           
                              
                  
             


                                        
                                                                               


                                                       


                      
                   

                                           




                                                  
                                   


                                                              


                           
                                                                          
                                                
 



                                 

                                                                
                              

                  
                                                         


                       
                   

                                           


                                                  

                                                     
                                   


                                                              


                           
                                                                          
                                                
 



                                   

                                                                
                              

                  





                                                
                                    
                                     
                                       


                      

                                                   



                                    

                              
                                
                                 
                                                         


                                                    
                                                                       
 






                                          
                                       










                                                                                
 
                                                        
 





                                        
                                       
                      
 
                                                             
                 
             
                                             
                                    
                            

                              
                                
                                 
                                   

                  
         


     





                          
                                                 
                                                                   
                                                       

 
                                                                           

                                                   
                          
                                          

                
                                          

                          


     





                                                                         















                                                 



















                                                                   


                                                


                                    
                      


          

                     




                                                

                         
                                    
                                                    

                                                   
                         


          




                                 
 
                          


                                   


                                                   
                        


          
                                                               

                       







                                




                                                           




































                                                                      
                                     



         


                                                                         

                                                        








                                       



                                 





                                        

 





                                                




                          












                                                                  



                                         
                                

                                                       




                                 



                                            
                                               






                               
                                          
                                 
                                                        

     
use crate::core::{
    Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
};
use crate::graphics::backend;
use crate::graphics::text;
use crate::graphics::{Damage, Viewport};
use crate::primitive::{self, Primitive};

use std::borrow::Cow;

pub struct Backend {
    text_pipeline: crate::text::Pipeline,

    #[cfg(feature = "image")]
    raster_pipeline: crate::raster::Pipeline,

    #[cfg(feature = "svg")]
    vector_pipeline: crate::vector::Pipeline,
}

impl Backend {
    pub fn new() -> Self {
        Self {
            text_pipeline: crate::text::Pipeline::new(),

            #[cfg(feature = "image")]
            raster_pipeline: crate::raster::Pipeline::new(),

            #[cfg(feature = "svg")]
            vector_pipeline: crate::vector::Pipeline::new(),
        }
    }

    pub fn draw<T: AsRef<str>>(
        &mut self,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: &mut tiny_skia::Mask,
        primitives: &[Primitive],
        viewport: &Viewport,
        damage: &[Rectangle],
        background_color: Color,
        overlay: &[T],
    ) {
        let physical_size = viewport.physical_size();
        let scale_factor = viewport.scale_factor() as f32;

        if !overlay.is_empty() {
            let path = tiny_skia::PathBuilder::from_rect(
                tiny_skia::Rect::from_xywh(
                    0.0,
                    0.0,
                    physical_size.width as f32,
                    physical_size.height as f32,
                )
                .expect("Create damage rectangle"),
            );

            pixels.fill_path(
                &path,
                &tiny_skia::Paint {
                    shader: tiny_skia::Shader::SolidColor(into_color(Color {
                        a: 0.1,
                        ..background_color
                    })),
                    anti_alias: false,
                    ..Default::default()
                },
                tiny_skia::FillRule::default(),
                tiny_skia::Transform::identity(),
                None,
            );
        }

        for &region in damage {
            let path = tiny_skia::PathBuilder::from_rect(
                tiny_skia::Rect::from_xywh(
                    region.x,
                    region.y,
                    region.width,
                    region.height,
                )
                .expect("Create damage rectangle"),
            );

            pixels.fill_path(
                &path,
                &tiny_skia::Paint {
                    shader: tiny_skia::Shader::SolidColor(into_color(
                        background_color,
                    )),
                    anti_alias: false,
                    blend_mode: tiny_skia::BlendMode::Source,
                    ..Default::default()
                },
                tiny_skia::FillRule::default(),
                tiny_skia::Transform::identity(),
                None,
            );

            adjust_clip_mask(clip_mask, region);

            for primitive in primitives {
                self.draw_primitive(
                    primitive,
                    pixels,
                    clip_mask,
                    region,
                    scale_factor,
                    Transformation::IDENTITY,
                );
            }

            if !overlay.is_empty() {
                pixels.stroke_path(
                    &path,
                    &tiny_skia::Paint {
                        shader: tiny_skia::Shader::SolidColor(into_color(
                            Color::from_rgb(1.0, 0.0, 0.0),
                        )),
                        anti_alias: false,
                        ..tiny_skia::Paint::default()
                    },
                    &tiny_skia::Stroke {
                        width: 1.0,
                        ..tiny_skia::Stroke::default()
                    },
                    tiny_skia::Transform::identity(),
                    None,
                );
            }
        }

        self.text_pipeline.trim_cache();

        #[cfg(feature = "image")]
        self.raster_pipeline.trim_cache();

        #[cfg(feature = "svg")]
        self.vector_pipeline.trim_cache();
    }

    fn draw_primitive(
        &mut self,
        primitive: &Primitive,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: &mut tiny_skia::Mask,
        clip_bounds: Rectangle,
        scale_factor: f32,
        transformation: Transformation,
    ) {
        match primitive {
            Primitive::Quad {
                bounds,
                background,
                border,
                shadow,
            } => {
                debug_assert!(
                    bounds.width.is_normal(),
                    "Quad with non-normal width!"
                );
                debug_assert!(
                    bounds.height.is_normal(),
                    "Quad with non-normal height!"
                );

                let physical_bounds = (*bounds * transformation) * scale_factor;

                if !clip_bounds.intersects(&physical_bounds) {
                    return;
                }

                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
                    .then_some(clip_mask as &_);

                let transform = into_transform(transformation)
                    .post_scale(scale_factor, scale_factor);

                // Make sure the border radius is not larger than the bounds
                let border_width = border
                    .width
                    .min(bounds.width / 2.0)
                    .min(bounds.height / 2.0);

                let mut fill_border_radius = <[f32; 4]>::from(border.radius);
                for radius in &mut fill_border_radius {
                    *radius = (*radius)
                        .min(bounds.width / 2.0)
                        .min(bounds.height / 2.0);
                }
                let path = rounded_rectangle(*bounds, fill_border_radius);

                if shadow.color.a > 0.0 {
                    let shadow_bounds = (Rectangle {
                        x: bounds.x + shadow.offset.x - shadow.blur_radius,
                        y: bounds.y + shadow.offset.y - shadow.blur_radius,
                        width: bounds.width + shadow.blur_radius * 2.0,
                        height: bounds.height + shadow.blur_radius * 2.0,
                    } * transformation)
                        * scale_factor;

                    let radii = fill_border_radius
                        .into_iter()
                        .map(|radius| radius * 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
                                                    * scale_factor)
                                                - half_width,
                                            y - physical_bounds.position().y
                                                - (shadow.offset.y
                                                    * scale_factor)
                                                - half_height,
                                        ),
                                        size,
                                        &radii,
                                    );
                                    let shadow_alpha = 1.0
                                        - smoothstep(
                                            -shadow.blur_radius * scale_factor,
                                            shadow.blur_radius * 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(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 {
                    // Border path is offset by half the border width
                    let border_bounds = Rectangle {
                        x: bounds.x + border_width / 2.0,
                        y: bounds.y + border_width / 2.0,
                        width: bounds.width - border_width,
                        height: bounds.height - border_width,
                    };

                    // Make sure the border radius is correct
                    let mut border_radius = <[f32; 4]>::from(border.radius);
                    let mut is_simple_border = true;

                    for radius in &mut border_radius {
                        *radius = if *radius == 0.0 {
                            // Path should handle this fine
                            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);
                    }

                    // Stroking a path works well in this case
                    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(border.color),
                                ),
                                anti_alias: true,
                                ..tiny_skia::Paint::default()
                            },
                            &tiny_skia::Stroke {
                                width: border_width,
                                ..tiny_skia::Stroke::default()
                            },
                            transform,
                            clip_mask,
                        );
                    } else {
                        // Draw corners that have too small border radii as having no border radius,
                        // but mask them with the rounded rectangle with the correct border radius.
                        let mut temp_pixmap = tiny_skia::Pixmap::new(
                            bounds.width as u32,
                            bounds.height as u32,
                        )
                        .unwrap();

                        let mut quad_mask = tiny_skia::Mask::new(
                            bounds.width as u32,
                            bounds.height as u32,
                        )
                        .unwrap();

                        let zero_bounds = Rectangle {
                            x: 0.0,
                            y: 0.0,
                            width: bounds.width,
                            height: 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: bounds.width - border_width,
                            height: 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(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(
                            bounds.x as i32,
                            bounds.y as i32,
                            temp_pixmap.as_ref(),
                            &tiny_skia::PixmapPaint::default(),
                            transform,
                            clip_mask,
                        );
                    }
                }
            }
            Primitive::Paragraph {
                paragraph,
                position,
                color,
                clip_bounds: _, // TODO: Support text clip bounds
            } => {
                let physical_bounds =
                    Rectangle::new(*position, paragraph.min_bounds)
                        * transformation
                        * scale_factor;

                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_paragraph(
                    paragraph,
                    *position,
                    *color,
                    scale_factor,
                    pixels,
                    clip_mask,
                    transformation,
                );
            }
            Primitive::Editor {
                editor,
                position,
                color,
                clip_bounds: _, // TODO: Support text clip bounds
            } => {
                let physical_bounds = Rectangle::new(*position, editor.bounds)
                    * transformation
                    * scale_factor;

                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_editor(
                    editor,
                    *position,
                    *color,
                    scale_factor,
                    pixels,
                    clip_mask,
                    transformation,
                );
            }
            Primitive::Text {
                content,
                bounds,
                color,
                size,
                line_height,
                font,
                horizontal_alignment,
                vertical_alignment,
                shaping,
                clip_bounds: _, // TODO: Support text clip bounds
            } => {
                let physical_bounds =
                    primitive.bounds() * transformation * scale_factor;

                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_cached(
                    content,
                    *bounds,
                    *color,
                    *size,
                    *line_height,
                    *font,
                    *horizontal_alignment,
                    *vertical_alignment,
                    *shaping,
                    scale_factor,
                    pixels,
                    clip_mask,
                    transformation,
                );
            }
            Primitive::RawText(text::Raw {
                buffer,
                position,
                color,
                clip_bounds: _, // TODO: Support text clip bounds
            }) => {
                let Some(buffer) = buffer.upgrade() else {
                    return;
                };

                let (width, height) = buffer.size();

                let physical_bounds =
                    Rectangle::new(*position, Size::new(width, height))
                        * transformation
                        * scale_factor;

                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,
                    *position,
                    *color,
                    scale_factor,
                    pixels,
                    clip_mask,
                    transformation,
                );
            }
            #[cfg(feature = "image")]
            Primitive::Image {
                handle,
                filter_method,
                bounds,
            } => {
                let physical_bounds = (*bounds * transformation) * scale_factor;

                if !clip_bounds.intersects(&physical_bounds) {
                    return;
                }

                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
                    .then_some(clip_mask as &_);

                let transform = into_transform(transformation)
                    .post_scale(scale_factor, scale_factor);

                self.raster_pipeline.draw(
                    handle,
                    *filter_method,
                    *bounds,
                    pixels,
                    transform,
                    clip_mask,
                );
            }
            #[cfg(not(feature = "image"))]
            Primitive::Image { .. } => {
                log::warn!(
                    "Unsupported primitive in `iced_tiny_skia`: {primitive:?}",
                );
            }
            #[cfg(feature = "svg")]
            Primitive::Svg {
                handle,
                bounds,
                color,
            } => {
                let physical_bounds = (*bounds * transformation) * scale_factor;

                if !clip_bounds.intersects(&physical_bounds) {
                    return;
                }

                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
                    .then_some(clip_mask as &_);

                self.vector_pipeline.draw(
                    handle,
                    *color,
                    (*bounds * transformation) * scale_factor,
                    pixels,
                    clip_mask,
                );
            }
            #[cfg(not(feature = "svg"))]
            Primitive::Svg { .. } => {
                log::warn!(
                    "Unsupported primitive in `iced_tiny_skia`: {primitive:?}",
                );
            }
            Primitive::Custom(primitive::Custom::Fill {
                path,
                paint,
                rule,
            }) => {
                let bounds = path.bounds();

                let physical_bounds = (Rectangle {
                    x: bounds.x(),
                    y: bounds.y(),
                    width: bounds.width(),
                    height: bounds.height(),
                } * transformation)
                    * scale_factor;

                if !clip_bounds.intersects(&physical_bounds) {
                    return;
                }

                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
                    .then_some(clip_mask as &_);

                pixels.fill_path(
                    path,
                    paint,
                    *rule,
                    into_transform(transformation)
                        .post_scale(scale_factor, scale_factor),
                    clip_mask,
                );
            }
            Primitive::Custom(primitive::Custom::Stroke {
                path,
                paint,
                stroke,
            }) => {
                let bounds = path.bounds();

                let physical_bounds = (Rectangle {
                    x: bounds.x(),
                    y: bounds.y(),
                    width: bounds.width().max(1.0),
                    height: bounds.height().max(1.0),
                } * transformation)
                    * scale_factor;

                if !clip_bounds.intersects(&physical_bounds) {
                    return;
                }

                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
                    .then_some(clip_mask as &_);

                pixels.stroke_path(
                    path,
                    paint,
                    stroke,
                    into_transform(transformation)
                        .post_scale(scale_factor, scale_factor),
                    clip_mask,
                );
            }
            Primitive::Group { primitives } => {
                for primitive in primitives {
                    self.draw_primitive(
                        primitive,
                        pixels,
                        clip_mask,
                        clip_bounds,
                        scale_factor,
                        transformation,
                    );
                }
            }
            Primitive::Transform {
                transformation: new_transformation,
                content,
            } => {
                self.draw_primitive(
                    content,
                    pixels,
                    clip_mask,
                    clip_bounds,
                    scale_factor,
                    transformation * *new_transformation,
                );
            }
            Primitive::Clip { bounds, content } => {
                let bounds = (*bounds * transformation) * scale_factor;

                if bounds == clip_bounds {
                    self.draw_primitive(
                        content,
                        pixels,
                        clip_mask,
                        bounds,
                        scale_factor,
                        transformation,
                    );
                } else if let Some(bounds) = clip_bounds.intersection(&bounds) {
                    if bounds.x + bounds.width <= 0.0
                        || bounds.y + bounds.height <= 0.0
                        || bounds.x as u32 >= pixels.width()
                        || bounds.y as u32 >= pixels.height()
                        || bounds.width <= 1.0
                        || bounds.height <= 1.0
                    {
                        return;
                    }

                    adjust_clip_mask(clip_mask, bounds);

                    self.draw_primitive(
                        content,
                        pixels,
                        clip_mask,
                        bounds,
                        scale_factor,
                        transformation,
                    );

                    adjust_clip_mask(clip_mask, clip_bounds);
                }
            }
            Primitive::Cache { content } => {
                self.draw_primitive(
                    content,
                    pixels,
                    clip_mask,
                    clip_bounds,
                    scale_factor,
                    transformation,
                );
            }
        }
    }
}

impl Default for Backend {
    fn default() -> Self {
        Self::new()
    }
}

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 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(),
    );
}

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
}

impl iced_graphics::Backend for Backend {
    type Primitive = primitive::Custom;
}

impl backend::Text for Backend {
    fn load_font(&mut self, font: Cow<'static, [u8]>) {
        self.text_pipeline.load_font(font);
    }
}

#[cfg(feature = "image")]
impl backend::Image for Backend {
    fn dimensions(
        &self,
        handle: &crate::core::image::Handle,
    ) -> crate::core::Size<u32> {
        self.raster_pipeline.dimensions(handle)
    }
}

#[cfg(feature = "svg")]
impl backend::Svg for Backend {
    fn viewport_dimensions(
        &self,
        handle: &crate::core::svg::Handle,
    ) -> crate::core::Size<u32> {
        self.vector_pipeline.viewport_dimensions(handle)
    }
}