Skip to main content

cranpose_render_common/
primitive_emit.rs

1use cranpose_ui_graphics::{
2    BlendMode, Brush, ColorFilter, DrawPrimitive, GraphicsLayer, ImageBitmap, Rect,
3    RoundedCornerShape, ShadowPrimitive,
4};
5
6use crate::graph::quad_bounds;
7use crate::layer_transform::{
8    apply_layer_affine_to_rect, apply_layer_to_quad, apply_layer_to_rect, layer_uniform_scale,
9};
10use crate::style_shared::{apply_layer_to_brush, compose_color_filters, scale_corner_radii};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum PrimitiveClipSpace {
14    Local,
15    LayerTransformed,
16}
17
18pub struct ShapeDrawParams {
19    pub rect: Rect,
20    pub local_rect: Rect,
21    pub quad: [[f32; 2]; 4],
22    pub brush: Brush,
23    pub shape: Option<RoundedCornerShape>,
24    pub clip: Option<Rect>,
25    pub blend_mode: BlendMode,
26}
27
28pub struct ImageDrawParams {
29    pub rect: Rect,
30    pub local_rect: Rect,
31    pub quad: [[f32; 2]; 4],
32    pub image: ImageBitmap,
33    pub alpha: f32,
34    pub color_filter: Option<ColorFilter>,
35    pub clip: Option<Rect>,
36    pub src_rect: Option<Rect>,
37    pub blend_mode: BlendMode,
38    pub motion_context_animated: bool,
39}
40
41pub trait DrawPrimitiveSink {
42    fn push_shape(&mut self, params: ShapeDrawParams);
43
44    fn push_image(&mut self, params: ImageDrawParams);
45
46    fn push_shadow(
47        &mut self,
48        shadow_primitive: ShadowPrimitive,
49        layer_bounds: Rect,
50        layer: &GraphicsLayer,
51        clip: Option<Rect>,
52    );
53}
54
55pub fn draw_shape_params_for_primitive(
56    primitive: DrawPrimitive,
57    layer_bounds: Rect,
58    layer: &GraphicsLayer,
59    clip: Option<Rect>,
60    blend_mode: BlendMode,
61) -> Option<ShapeDrawParams> {
62    struct SingleShapeSink {
63        shape: Option<ShapeDrawParams>,
64    }
65
66    impl DrawPrimitiveSink for SingleShapeSink {
67        fn push_shape(&mut self, params: ShapeDrawParams) {
68            if self.shape.is_none() {
69                self.shape = Some(params);
70            }
71        }
72
73        fn push_image(&mut self, _params: ImageDrawParams) {}
74
75        fn push_shadow(
76            &mut self,
77            _shadow_primitive: ShadowPrimitive,
78            _layer_bounds: Rect,
79            _layer: &GraphicsLayer,
80            _clip: Option<Rect>,
81        ) {
82        }
83    }
84
85    let mut sink = SingleShapeSink { shape: None };
86    emit_draw_primitive(
87        primitive,
88        layer_bounds,
89        layer,
90        clip,
91        &mut sink,
92        Some(blend_mode),
93        false,
94    );
95    sink.shape
96}
97
98pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
99    match (parent_clip, requested_clip) {
100        (Some(parent), Some(current)) => parent.intersect(current),
101        (Some(parent), None) => Some(parent),
102        (None, Some(current)) => Some(current),
103        (None, None) => None,
104    }
105}
106
107pub fn resolve_primitive_clip(
108    local_clip: Option<Rect>,
109    layer_bounds: Rect,
110    layer: &GraphicsLayer,
111    parent_clip: Option<Rect>,
112    clip_space: PrimitiveClipSpace,
113) -> Option<Rect> {
114    let Some(local_clip) = local_clip else {
115        return parent_clip;
116    };
117    let clip_rect = Rect {
118        x: layer_bounds.x + local_clip.x,
119        y: layer_bounds.y + local_clip.y,
120        width: local_clip.width,
121        height: local_clip.height,
122    };
123    let requested_clip = match clip_space {
124        PrimitiveClipSpace::Local => clip_rect,
125        PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
126    };
127    resolve_clip(parent_clip, Some(requested_clip))
128}
129
130pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
131    primitive: DrawPrimitive,
132    layer_bounds: Rect,
133    layer: &GraphicsLayer,
134    clip: Option<Rect>,
135    sink: &mut S,
136    blend_mode: Option<BlendMode>,
137    motion_context_animated: bool,
138) {
139    match primitive {
140        DrawPrimitive::Content => {}
141        DrawPrimitive::Blend {
142            primitive,
143            blend_mode: nested,
144        } => emit_draw_primitive(
145            *primitive,
146            layer_bounds,
147            layer,
148            clip,
149            sink,
150            blend_mode.or(Some(nested)),
151            motion_context_animated,
152        ),
153        DrawPrimitive::Rect {
154            rect: local_rect,
155            brush,
156        } => {
157            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
158            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
159            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
160            sink.push_shape(ShapeDrawParams {
161                rect: quad_bounds(quad),
162                local_rect,
163                quad,
164                brush: apply_layer_to_brush(brush, layer),
165                shape: None,
166                clip,
167                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
168            });
169        }
170        DrawPrimitive::RoundRect {
171            rect: local_rect,
172            brush,
173            radii,
174        } => {
175            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
176            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
177            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
178            let shape = RoundedCornerShape::with_radii(scale_corner_radii(
179                radii,
180                layer_uniform_scale(layer),
181            ));
182            sink.push_shape(ShapeDrawParams {
183                rect: quad_bounds(quad),
184                local_rect,
185                quad,
186                brush: apply_layer_to_brush(brush, layer),
187                shape: Some(shape),
188                clip,
189                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
190            });
191        }
192        DrawPrimitive::Image {
193            rect: local_rect,
194            image,
195            alpha,
196            color_filter,
197            src_rect,
198        } => {
199            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
200            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
201            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
202            sink.push_image(ImageDrawParams {
203                rect: quad_bounds(quad),
204                local_rect,
205                quad,
206                image,
207                alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
208                color_filter: compose_color_filters(color_filter, layer.color_filter),
209                clip,
210                src_rect,
211                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
212                motion_context_animated,
213            });
214        }
215        DrawPrimitive::Shadow(shadow_primitive) => {
216            sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use cranpose_ui_graphics::{Brush, Color, CornerRadii};
225
226    #[test]
227    fn draw_shape_params_for_primitive_returns_transformed_rect_shape() {
228        let shape = draw_shape_params_for_primitive(
229            DrawPrimitive::Rect {
230                rect: Rect {
231                    x: 2.0,
232                    y: 3.0,
233                    width: 8.0,
234                    height: 5.0,
235                },
236                brush: Brush::solid(Color::WHITE),
237            },
238            Rect {
239                x: 10.0,
240                y: 20.0,
241                width: 40.0,
242                height: 30.0,
243            },
244            &GraphicsLayer::default(),
245            None,
246            BlendMode::SrcOver,
247        )
248        .expect("rect shape");
249
250        assert_eq!(
251            shape.rect,
252            Rect {
253                x: 12.0,
254                y: 23.0,
255                width: 8.0,
256                height: 5.0,
257            }
258        );
259        assert!(shape.shape.is_none());
260    }
261
262    #[test]
263    fn draw_shape_params_for_primitive_resolves_blended_round_rect() {
264        let shape = draw_shape_params_for_primitive(
265            DrawPrimitive::Blend {
266                primitive: Box::new(DrawPrimitive::RoundRect {
267                    rect: Rect {
268                        x: 1.0,
269                        y: 1.0,
270                        width: 10.0,
271                        height: 6.0,
272                    },
273                    brush: Brush::solid(Color::BLACK),
274                    radii: CornerRadii::uniform(4.0),
275                }),
276                blend_mode: BlendMode::DstOut,
277            },
278            Rect::from_size(cranpose_ui_graphics::Size {
279                width: 20.0,
280                height: 20.0,
281            }),
282            &GraphicsLayer::default(),
283            None,
284            BlendMode::SrcOver,
285        )
286        .expect("round rect shape");
287
288        assert_eq!(shape.blend_mode, BlendMode::SrcOver);
289        assert!(shape.shape.is_some());
290    }
291
292    #[test]
293    fn draw_shape_params_for_primitive_rejects_non_shape_primitives() {
294        assert!(draw_shape_params_for_primitive(
295            DrawPrimitive::Image {
296                rect: Rect::from_size(cranpose_ui_graphics::Size {
297                    width: 4.0,
298                    height: 4.0,
299                }),
300                image: cranpose_ui_graphics::ImageBitmap::from_rgba8(
301                    1,
302                    1,
303                    vec![255, 255, 255, 255],
304                )
305                .expect("image"),
306                alpha: 1.0,
307                color_filter: None,
308                src_rect: None,
309            },
310            Rect::from_size(cranpose_ui_graphics::Size {
311                width: 10.0,
312                height: 10.0,
313            }),
314            &GraphicsLayer::default(),
315            None,
316            BlendMode::SrcOver,
317        )
318        .is_none());
319    }
320}