summaryrefslogblamecommitdiffstats
path: root/tiny_skia/src/geometry.rs
blob: dbdff444456d6f5f6e541d8b9957041733c9c207 (plain) (tree)
1
2
3
4
5
6
7
8
                     
                                  
                                                                              
                                           

                                                      

                                                   






                        
                                     








                                   
                                      










                                    
                                                                             


                           
                       



                                                 
                                         






                                        
 
                
                  
                           


                                     
                                 
                    



                                    



                                                      
              
                        

                                   
                               




                                                            



         
                                         
                             

                            
                              

     
                             
                               

     
                            
                               

     
                               
                                                                               

     
                                                            


                                                                              

                   
 

                               


                                               




                                              

     
                      




                              


                                                                       

                   
 

                               





                                               




                                              

     
                                                                          

                                                                              






















                                                 
                                                              

     
                                                              

                               

                                                            
                                     































                                                                               
 




                                      

              
                                    
                                         



                                      
                                                           








                                                                  

     
                                  


                                        
                                 


                                                                  
                                                         
                                    

     
                                      

                                                 
                                         
     
 
                                                  



                                                                       
                                                     


                                                                           

     
                                                

                                 
                                                             

     
                                                              

                                 


                                                                    


                                        
                                


                                          
     
 
                                                                                

                                     


                                                        


                                                                 

     


                                                                    


                                                        


                                                     






























                                                 

 
                                                         
                                                 

                                                    
                                                           
 
                             














































                                                                              







                                                     

 
                                                              






                                                                               

                                                                    
                              
                                    
                                  











                                                            























                                                              

              
                         










                                                                
                                                              






















                                                                  
use crate::Primitive;
use crate::core::text::LineHeight;
use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector};
use crate::graphics::cache::{self, Cached};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
use crate::graphics::geometry::{self, Path, Style};
use crate::graphics::{self, Gradient, Image, Text};

use std::rc::Rc;

#[derive(Debug)]
pub enum Geometry {
    Live {
        text: Vec<Text>,
        images: Vec<graphics::Image>,
        primitives: Vec<Primitive>,
        clip_bounds: Rectangle,
    },
    Cache(Cache),
}

#[derive(Debug, Clone)]
pub struct Cache {
    pub text: Rc<[Text]>,
    pub images: Rc<[graphics::Image]>,
    pub primitives: Rc<[Primitive]>,
    pub clip_bounds: Rectangle,
}

impl Cached for Geometry {
    type Cache = Cache;

    fn load(cache: &Cache) -> Self {
        Self::Cache(cache.clone())
    }

    fn cache(self, _group: cache::Group, _previous: Option<Cache>) -> Cache {
        match self {
            Self::Live {
                primitives,
                images,
                text,
                clip_bounds,
            } => Cache {
                primitives: Rc::from(primitives),
                images: Rc::from(images),
                text: Rc::from(text),
                clip_bounds,
            },
            Self::Cache(cache) => cache,
        }
    }
}

#[derive(Debug)]
pub struct Frame {
    clip_bounds: Rectangle,
    transform: tiny_skia::Transform,
    stack: Vec<tiny_skia::Transform>,
    primitives: Vec<Primitive>,
    images: Vec<graphics::Image>,
    text: Vec<Text>,
}

impl Frame {
    pub fn new(size: Size) -> Self {
        Self::with_clip(Rectangle::with_size(size))
    }

    pub fn with_clip(clip_bounds: Rectangle) -> Self {
        Self {
            clip_bounds,
            stack: Vec::new(),
            primitives: Vec::new(),
            images: Vec::new(),
            text: Vec::new(),
            transform: tiny_skia::Transform::from_translate(
                clip_bounds.x,
                clip_bounds.y,
            ),
        }
    }
}

impl geometry::frame::Backend for Frame {
    type Geometry = Geometry;

    fn width(&self) -> f32 {
        self.clip_bounds.width
    }

    fn height(&self) -> f32 {
        self.clip_bounds.height
    }

    fn size(&self) -> Size {
        self.clip_bounds.size()
    }

    fn center(&self) -> Point {
        Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
    }

    fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
        let Some(path) =
            convert_path(path).and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let fill = fill.into();

        let mut paint = into_paint(fill.style);
        paint.shader.transform(self.transform);

        self.primitives.push(Primitive::Fill {
            path,
            paint,
            rule: into_fill_rule(fill.rule),
        });
    }

    fn fill_rectangle(
        &mut self,
        top_left: Point,
        size: Size,
        fill: impl Into<Fill>,
    ) {
        let Some(path) = convert_path(&Path::rectangle(top_left, size))
            .and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let fill = fill.into();

        let mut paint = tiny_skia::Paint {
            anti_alias: false,
            ..into_paint(fill.style)
        };
        paint.shader.transform(self.transform);

        self.primitives.push(Primitive::Fill {
            path,
            paint,
            rule: into_fill_rule(fill.rule),
        });
    }

    fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
        let Some(path) =
            convert_path(path).and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let stroke = stroke.into();
        let skia_stroke = into_stroke(&stroke);

        let mut paint = into_paint(stroke.style);
        paint.shader.transform(self.transform);

        self.primitives.push(Primitive::Stroke {
            path,
            paint,
            stroke: skia_stroke,
        });
    }

    fn stroke_rectangle<'a>(
        &mut self,
        top_left: Point,
        size: Size,
        stroke: impl Into<Stroke<'a>>,
    ) {
        self.stroke(&Path::rectangle(top_left, size), stroke);
    }

    fn fill_text(&mut self, text: impl Into<geometry::Text>) {
        let text = text.into();

        let (scale_x, scale_y) = self.transform.get_scale();

        if !self.transform.has_skew()
            && scale_x == scale_y
            && scale_x > 0.0
            && scale_y > 0.0
        {
            let (position, size, line_height) = if self.transform.is_identity()
            {
                (text.position, text.size, text.line_height)
            } else {
                let mut position = [tiny_skia::Point {
                    x: text.position.x,
                    y: text.position.y,
                }];

                self.transform.map_points(&mut position);

                let size = text.size.0 * scale_y;

                let line_height = match text.line_height {
                    LineHeight::Absolute(size) => {
                        LineHeight::Absolute(Pixels(size.0 * scale_y))
                    }
                    LineHeight::Relative(factor) => {
                        LineHeight::Relative(factor)
                    }
                };

                (
                    Point::new(position[0].x, position[0].y),
                    size.into(),
                    line_height,
                )
            };

            let bounds = Rectangle {
                x: position.x,
                y: position.y,
                width: f32::INFINITY,
                height: f32::INFINITY,
            };

            // TODO: Honor layering!
            self.text.push(Text::Cached {
                content: text.content,
                bounds,
                color: text.color,
                size,
                line_height: line_height.to_absolute(size),
                font: text.font,
                horizontal_alignment: text.horizontal_alignment,
                vertical_alignment: text.vertical_alignment,
                shaping: text.shaping,
                clip_bounds: Rectangle::with_size(Size::INFINITY),
            });
        } else {
            text.draw_with(|path, color| self.fill(&path, color));
        }
    }

    fn push_transform(&mut self) {
        self.stack.push(self.transform);
    }

    fn pop_transform(&mut self) {
        self.transform = self.stack.pop().expect("Pop transform");
    }

    fn draft(&mut self, clip_bounds: Rectangle) -> Self {
        Self::with_clip(clip_bounds)
    }

    fn paste(&mut self, frame: Self) {
        self.primitives.extend(frame.primitives);
        self.text.extend(frame.text);
        self.images.extend(frame.images);
    }

    fn translate(&mut self, translation: Vector) {
        self.transform =
            self.transform.pre_translate(translation.x, translation.y);
    }

    fn rotate(&mut self, angle: impl Into<Radians>) {
        self.transform = self.transform.pre_concat(
            tiny_skia::Transform::from_rotate(angle.into().0.to_degrees()),
        );
    }

    fn scale(&mut self, scale: impl Into<f32>) {
        let scale = scale.into();

        self.scale_nonuniform(Vector { x: scale, y: scale });
    }

    fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
        let scale = scale.into();

        self.transform = self.transform.pre_scale(scale.x, scale.y);
    }

    fn into_geometry(self) -> Geometry {
        Geometry::Live {
            primitives: self.primitives,
            images: self.images,
            text: self.text,
            clip_bounds: self.clip_bounds,
        }
    }

    fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) {
        let mut image = image.into();

        let (bounds, external_rotation) =
            transform_rectangle(bounds, self.transform);

        image.rotation += external_rotation;

        self.images.push(graphics::Image::Raster(image, bounds));
    }

    fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
        let mut svg = svg.into();

        let (bounds, external_rotation) =
            transform_rectangle(bounds, self.transform);

        svg.rotation += external_rotation;

        self.images.push(Image::Vector(svg, bounds));
    }
}

fn transform_rectangle(
    rectangle: Rectangle,
    transform: tiny_skia::Transform,
) -> (Rectangle, Radians) {
    let mut top_left = tiny_skia::Point {
        x: rectangle.x,
        y: rectangle.y,
    };

    let mut top_right = tiny_skia::Point {
        x: rectangle.x + rectangle.width,
        y: rectangle.y,
    };

    let mut bottom_left = tiny_skia::Point {
        x: rectangle.x,
        y: rectangle.y + rectangle.height,
    };

    transform.map_point(&mut top_left);
    transform.map_point(&mut top_right);
    transform.map_point(&mut bottom_left);

    Rectangle::with_vertices(
        Point::new(top_left.x, top_left.y),
        Point::new(top_right.x, top_right.y),
        Point::new(bottom_left.x, bottom_left.y),
    )
}

fn convert_path(path: &Path) -> Option<tiny_skia::Path> {
    use iced_graphics::geometry::path::lyon_path;

    let mut builder = tiny_skia::PathBuilder::new();
    let mut last_point = lyon_path::math::Point::default();

    for event in path.raw() {
        match event {
            lyon_path::Event::Begin { at } => {
                builder.move_to(at.x, at.y);

                last_point = at;
            }
            lyon_path::Event::Line { from, to } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder.line_to(to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::Quadratic { from, ctrl, to } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder.quad_to(ctrl.x, ctrl.y, to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::Cubic {
                from,
                ctrl1,
                ctrl2,
                to,
            } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder
                    .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::End { close, .. } => {
                if close {
                    builder.close();
                }
            }
        }
    }

    let result = builder.finish();

    #[cfg(debug_assertions)]
    if result.is_none() {
        log::warn!("Invalid path: {:?}", path.raw());
    }

    result
}

pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> {
    tiny_skia::Paint {
        shader: match style {
            Style::Solid(color) => tiny_skia::Shader::SolidColor(
                tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
                    .expect("Create color"),
            ),
            Style::Gradient(gradient) => match gradient {
                Gradient::Linear(linear) => {
                    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: linear.start.x,
                            y: linear.start.y,
                        },
                        tiny_skia::Point {
                            x: linear.end.x,
                            y: linear.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,
        ..Default::default()
    }
}

pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule {
    match rule {
        fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd,
        fill::Rule::NonZero => tiny_skia::FillRule::Winding,
    }
}

pub fn into_stroke(stroke: &Stroke<'_>) -> tiny_skia::Stroke {
    tiny_skia::Stroke {
        width: stroke.width,
        line_cap: match stroke.line_cap {
            stroke::LineCap::Butt => tiny_skia::LineCap::Butt,
            stroke::LineCap::Square => tiny_skia::LineCap::Square,
            stroke::LineCap::Round => tiny_skia::LineCap::Round,
        },
        line_join: match stroke.line_join {
            stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter,
            stroke::LineJoin::Round => tiny_skia::LineJoin::Round,
            stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
        },
        dash: if stroke.line_dash.segments.is_empty() {
            None
        } else {
            tiny_skia::StrokeDash::new(
                stroke.line_dash.segments.into(),
                stroke.line_dash.offset as f32,
            )
        },
        ..Default::default()
    }
}