cranpose-render-common 0.0.58

Common rendering contracts for Cranpose
Documentation
use cranpose_ui_graphics::{
    BlendMode, Brush, ColorFilter, DrawPrimitive, GraphicsLayer, ImageBitmap, Rect,
    RoundedCornerShape, ShadowPrimitive,
};

use crate::graph::quad_bounds;
use crate::layer_transform::{
    apply_layer_affine_to_rect, apply_layer_to_quad, apply_layer_to_rect, layer_uniform_scale,
};
use crate::style_shared::{apply_layer_to_brush, compose_color_filters, scale_corner_radii};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrimitiveClipSpace {
    Local,
    LayerTransformed,
}

pub struct ShapeDrawParams {
    pub rect: Rect,
    pub local_rect: Rect,
    pub quad: [[f32; 2]; 4],
    pub brush: Brush,
    pub shape: Option<RoundedCornerShape>,
    pub clip: Option<Rect>,
    pub blend_mode: BlendMode,
}

pub struct ImageDrawParams {
    pub rect: Rect,
    pub local_rect: Rect,
    pub quad: [[f32; 2]; 4],
    pub image: ImageBitmap,
    pub alpha: f32,
    pub color_filter: Option<ColorFilter>,
    pub clip: Option<Rect>,
    pub src_rect: Option<Rect>,
    pub blend_mode: BlendMode,
    pub motion_context_animated: bool,
}

pub trait DrawPrimitiveSink {
    fn push_shape(&mut self, params: ShapeDrawParams);

    fn push_image(&mut self, params: ImageDrawParams);

    fn push_shadow(
        &mut self,
        shadow_primitive: ShadowPrimitive,
        layer_bounds: Rect,
        layer: &GraphicsLayer,
        clip: Option<Rect>,
    );
}

pub fn draw_shape_params_for_primitive(
    primitive: DrawPrimitive,
    layer_bounds: Rect,
    layer: &GraphicsLayer,
    clip: Option<Rect>,
    blend_mode: BlendMode,
) -> Option<ShapeDrawParams> {
    struct SingleShapeSink {
        shape: Option<ShapeDrawParams>,
    }

    impl DrawPrimitiveSink for SingleShapeSink {
        fn push_shape(&mut self, params: ShapeDrawParams) {
            if self.shape.is_none() {
                self.shape = Some(params);
            }
        }

        fn push_image(&mut self, _params: ImageDrawParams) {}

        fn push_shadow(
            &mut self,
            _shadow_primitive: ShadowPrimitive,
            _layer_bounds: Rect,
            _layer: &GraphicsLayer,
            _clip: Option<Rect>,
        ) {
        }
    }

    let mut sink = SingleShapeSink { shape: None };
    emit_draw_primitive(
        primitive,
        layer_bounds,
        layer,
        clip,
        &mut sink,
        Some(blend_mode),
        false,
    );
    sink.shape
}

pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
    match (parent_clip, requested_clip) {
        (Some(parent), Some(current)) => parent.intersect(current),
        (Some(parent), None) => Some(parent),
        (None, Some(current)) => Some(current),
        (None, None) => None,
    }
}

pub fn resolve_primitive_clip(
    local_clip: Option<Rect>,
    layer_bounds: Rect,
    layer: &GraphicsLayer,
    parent_clip: Option<Rect>,
    clip_space: PrimitiveClipSpace,
) -> Option<Rect> {
    let Some(local_clip) = local_clip else {
        return parent_clip;
    };
    let clip_rect = Rect {
        x: layer_bounds.x + local_clip.x,
        y: layer_bounds.y + local_clip.y,
        width: local_clip.width,
        height: local_clip.height,
    };
    let requested_clip = match clip_space {
        PrimitiveClipSpace::Local => clip_rect,
        PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
    };
    resolve_clip(parent_clip, Some(requested_clip))
}

pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
    primitive: DrawPrimitive,
    layer_bounds: Rect,
    layer: &GraphicsLayer,
    clip: Option<Rect>,
    sink: &mut S,
    blend_mode: Option<BlendMode>,
    motion_context_animated: bool,
) {
    match primitive {
        DrawPrimitive::Content => {}
        DrawPrimitive::Blend {
            primitive,
            blend_mode: nested,
        } => emit_draw_primitive(
            *primitive,
            layer_bounds,
            layer,
            clip,
            sink,
            blend_mode.or(Some(nested)),
            motion_context_animated,
        ),
        DrawPrimitive::Rect {
            rect: local_rect,
            brush,
        } => {
            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
            sink.push_shape(ShapeDrawParams {
                rect: quad_bounds(quad),
                local_rect,
                quad,
                brush: apply_layer_to_brush(brush, layer),
                shape: None,
                clip,
                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
            });
        }
        DrawPrimitive::RoundRect {
            rect: local_rect,
            brush,
            radii,
        } => {
            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
            let shape = RoundedCornerShape::with_radii(scale_corner_radii(
                radii,
                layer_uniform_scale(layer),
            ));
            sink.push_shape(ShapeDrawParams {
                rect: quad_bounds(quad),
                local_rect,
                quad,
                brush: apply_layer_to_brush(brush, layer),
                shape: Some(shape),
                clip,
                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
            });
        }
        DrawPrimitive::Image {
            rect: local_rect,
            image,
            alpha,
            color_filter,
            src_rect,
        } => {
            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
            sink.push_image(ImageDrawParams {
                rect: quad_bounds(quad),
                local_rect,
                quad,
                image,
                alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
                color_filter: compose_color_filters(color_filter, layer.color_filter),
                clip,
                src_rect,
                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
                motion_context_animated,
            });
        }
        DrawPrimitive::Shadow(shadow_primitive) => {
            sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cranpose_ui_graphics::{Brush, Color, CornerRadii};

    #[test]
    fn draw_shape_params_for_primitive_returns_transformed_rect_shape() {
        let shape = draw_shape_params_for_primitive(
            DrawPrimitive::Rect {
                rect: Rect {
                    x: 2.0,
                    y: 3.0,
                    width: 8.0,
                    height: 5.0,
                },
                brush: Brush::solid(Color::WHITE),
            },
            Rect {
                x: 10.0,
                y: 20.0,
                width: 40.0,
                height: 30.0,
            },
            &GraphicsLayer::default(),
            None,
            BlendMode::SrcOver,
        )
        .expect("rect shape");

        assert_eq!(
            shape.rect,
            Rect {
                x: 12.0,
                y: 23.0,
                width: 8.0,
                height: 5.0,
            }
        );
        assert!(shape.shape.is_none());
    }

    #[test]
    fn draw_shape_params_for_primitive_resolves_blended_round_rect() {
        let shape = draw_shape_params_for_primitive(
            DrawPrimitive::Blend {
                primitive: Box::new(DrawPrimitive::RoundRect {
                    rect: Rect {
                        x: 1.0,
                        y: 1.0,
                        width: 10.0,
                        height: 6.0,
                    },
                    brush: Brush::solid(Color::BLACK),
                    radii: CornerRadii::uniform(4.0),
                }),
                blend_mode: BlendMode::DstOut,
            },
            Rect::from_size(cranpose_ui_graphics::Size {
                width: 20.0,
                height: 20.0,
            }),
            &GraphicsLayer::default(),
            None,
            BlendMode::SrcOver,
        )
        .expect("round rect shape");

        assert_eq!(shape.blend_mode, BlendMode::SrcOver);
        assert!(shape.shape.is_some());
    }

    #[test]
    fn draw_shape_params_for_primitive_rejects_non_shape_primitives() {
        assert!(draw_shape_params_for_primitive(
            DrawPrimitive::Image {
                rect: Rect::from_size(cranpose_ui_graphics::Size {
                    width: 4.0,
                    height: 4.0,
                }),
                image: cranpose_ui_graphics::ImageBitmap::from_rgba8(
                    1,
                    1,
                    vec![255, 255, 255, 255],
                )
                .expect("image"),
                alpha: 1.0,
                color_filter: None,
                src_rect: None,
            },
            Rect::from_size(cranpose_ui_graphics::Size {
                width: 10.0,
                height: 10.0,
            }),
            &GraphicsLayer::default(),
            None,
            BlendMode::SrcOver,
        )
        .is_none());
    }
}