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