Skip to main content

cranpose_render_common/
primitive_emit.rs

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