Skip to main content

cranpose_render_pixels/
style.rs

1#[cfg(test)]
2pub(crate) use cranpose_render_common::graph::quad_bounds;
3#[cfg(test)]
4pub(crate) use cranpose_render_common::layer_transform::{
5    apply_layer_affine_to_rect, apply_layer_to_quad,
6};
7#[cfg(test)]
8pub(crate) use cranpose_render_common::layer_transform::{
9    apply_layer_to_rect, layer_uniform_scale,
10};
11pub(crate) use cranpose_render_common::style_shared::{
12    apply_layer_to_brush, apply_layer_to_color, combine_layers, scale_corner_radii,
13};
14#[cfg(test)]
15pub(crate) use cranpose_render_common::style_shared::{
16    compose_color_filters, primitives_for_placement, DrawPlacement,
17};
18#[cfg(test)]
19use cranpose_ui::DrawCommand;
20#[cfg(test)]
21use cranpose_ui_graphics::RoundedCornerShape;
22#[cfg(test)]
23use cranpose_ui_graphics::{BlendMode, DrawPrimitive, GraphicsLayer, ShadowPrimitive, Size};
24use cranpose_ui_graphics::{CornerRadii, Rect};
25
26#[cfg(test)]
27use crate::scene::RasterScene;
28
29#[cfg(test)]
30#[allow(clippy::too_many_arguments)] // Render operations need all style and placement parameters
31pub(crate) fn apply_draw_commands(
32    commands: &[DrawCommand],
33    placement: DrawPlacement,
34    rect: Rect,
35    size: Size,
36    layer: &GraphicsLayer,
37    clip: Option<Rect>,
38    scene: &mut RasterScene,
39) {
40    fn emit_primitive(
41        primitive: DrawPrimitive,
42        layer_bounds: Rect,
43        layer: &GraphicsLayer,
44        clip: Option<Rect>,
45        scene: &mut RasterScene,
46        blend_mode: Option<BlendMode>,
47    ) {
48        match primitive {
49            DrawPrimitive::Content => {}
50            DrawPrimitive::Blend {
51                primitive,
52                blend_mode: nested,
53            } => emit_primitive(
54                *primitive,
55                layer_bounds,
56                layer,
57                clip,
58                scene,
59                blend_mode.or(Some(nested)),
60            ),
61            DrawPrimitive::Rect {
62                rect: local_rect,
63                brush,
64            } => {
65                let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
66                let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
67                let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
68                let transformed = quad_bounds(quad);
69                let brush = apply_layer_to_brush(brush, layer);
70                scene.push_shape_with_geometry(
71                    transformed,
72                    local_rect,
73                    quad,
74                    brush,
75                    None,
76                    clip,
77                    blend_mode.unwrap_or(BlendMode::SrcOver),
78                );
79            }
80            DrawPrimitive::RoundRect {
81                rect: local_rect,
82                brush,
83                radii,
84            } => {
85                let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
86                let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
87                let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
88                let transformed = quad_bounds(quad);
89                let scaled_radii = scale_corner_radii(radii, layer_uniform_scale(layer));
90                let shape = RoundedCornerShape::with_radii(scaled_radii);
91                let brush = apply_layer_to_brush(brush, layer);
92                scene.push_shape_with_geometry(
93                    transformed,
94                    local_rect,
95                    quad,
96                    brush,
97                    Some(shape),
98                    clip,
99                    blend_mode.unwrap_or(BlendMode::SrcOver),
100                );
101            }
102            DrawPrimitive::Image {
103                rect: local_rect,
104                image,
105                alpha,
106                color_filter,
107                sampling,
108                src_rect,
109            } => {
110                let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
111                let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
112                let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
113                let transformed = quad_bounds(quad);
114                let combined_alpha = (alpha * layer.alpha).clamp(0.0, 1.0);
115                let combined_filter = compose_color_filters(color_filter, layer.color_filter);
116                scene.push_image_with_geometry(
117                    transformed,
118                    local_rect,
119                    quad,
120                    image,
121                    combined_alpha,
122                    combined_filter,
123                    sampling,
124                    clip,
125                    src_rect,
126                    blend_mode.unwrap_or(BlendMode::SrcOver),
127                );
128            }
129            DrawPrimitive::Shadow(shadow_primitive) => match shadow_primitive {
130                ShadowPrimitive::Drop {
131                    shape,
132                    blur_radius: _,
133                    blend_mode: shadow_blend_mode,
134                } => {
135                    // Pixels renderer currently ignores blur radius and renders the base
136                    // shadow geometry directly.
137                    emit_primitive(
138                        *shape,
139                        layer_bounds,
140                        layer,
141                        clip,
142                        scene,
143                        blend_mode.or(Some(shadow_blend_mode)),
144                    );
145                }
146                ShadowPrimitive::Inner {
147                    fill,
148                    cutout,
149                    blur_radius: _,
150                    blend_mode: shadow_blend_mode,
151                    clip_rect,
152                } => {
153                    let abs_clip = Rect {
154                        x: clip_rect.x + layer_bounds.x,
155                        y: clip_rect.y + layer_bounds.y,
156                        width: clip_rect.width,
157                        height: clip_rect.height,
158                    };
159                    let transformed_clip = apply_layer_to_rect(abs_clip, layer_bounds, layer);
160                    let shadow_clip = clip.map_or(Some(transformed_clip), |parent_clip| {
161                        parent_clip.intersect(transformed_clip)
162                    });
163                    emit_primitive(
164                        *fill,
165                        layer_bounds,
166                        layer,
167                        shadow_clip,
168                        scene,
169                        blend_mode.or(Some(shadow_blend_mode)),
170                    );
171                    emit_primitive(
172                        *cutout,
173                        layer_bounds,
174                        layer,
175                        shadow_clip,
176                        scene,
177                        blend_mode.or(Some(BlendMode::DstOut)),
178                    );
179                }
180            },
181        }
182    }
183
184    for command in commands {
185        let primitives = primitives_for_placement(command, placement, size);
186        for primitive in primitives {
187            emit_primitive(primitive, rect, layer, clip, scene, None);
188        }
189    }
190}
191
192pub(crate) fn point_in_resolved_rounded_rect(
193    x: f32,
194    y: f32,
195    rect: Rect,
196    radii: &CornerRadii,
197) -> bool {
198    if !rect.contains(x, y) {
199        return false;
200    }
201    let left = rect.x;
202    let right = rect.x + rect.width;
203    let top = rect.y;
204    let bottom = rect.y + rect.height;
205
206    if radii.top_left > 0.0 && x < left + radii.top_left && y < top + radii.top_left {
207        let cx = left + radii.top_left;
208        let cy = top + radii.top_left;
209        if (x - cx).powi(2) + (y - cy).powi(2) > radii.top_left.powi(2) {
210            return false;
211        }
212    }
213    if radii.top_right > 0.0 && x > right - radii.top_right && y < top + radii.top_right {
214        let cx = right - radii.top_right;
215        let cy = top + radii.top_right;
216        if (x - cx).powi(2) + (y - cy).powi(2) > radii.top_right.powi(2) {
217            return false;
218        }
219    }
220    if radii.bottom_right > 0.0 && x > right - radii.bottom_right && y > bottom - radii.bottom_right
221    {
222        let cx = right - radii.bottom_right;
223        let cy = bottom - radii.bottom_right;
224        if (x - cx).powi(2) + (y - cy).powi(2) > radii.bottom_right.powi(2) {
225            return false;
226        }
227    }
228    if radii.bottom_left > 0.0 && x < left + radii.bottom_left && y > bottom - radii.bottom_left {
229        let cx = left + radii.bottom_left;
230        let cy = bottom - radii.bottom_left;
231        if (x - cx).powi(2) + (y - cy).powi(2) > radii.bottom_left.powi(2) {
232            return false;
233        }
234    }
235    true
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use cranpose_ui::Brush;
242    use cranpose_ui_graphics::{
243        Color, ColorFilter, CompositingStrategy, LayerShape, RenderEffect, RoundedCornerShape,
244        TransformOrigin,
245    };
246
247    #[test]
248    fn combine_layers_clears_effects_without_new_layer() {
249        let current = GraphicsLayer {
250            alpha: 0.7,
251            scale: 1.2,
252            translation_x: 4.0,
253            translation_y: 6.0,
254            color_filter: None,
255            render_effect: Some(RenderEffect::blur(4.0)),
256            backdrop_effect: Some(RenderEffect::blur(2.0)),
257            ..Default::default()
258        };
259
260        let combined = combine_layers(current.clone(), None);
261        assert_eq!(combined.alpha, current.alpha);
262        assert_eq!(combined.scale, current.scale);
263        assert_eq!(combined.translation_x, current.translation_x);
264        assert_eq!(combined.translation_y, current.translation_y);
265        assert_eq!(combined.compositing_strategy, CompositingStrategy::Auto);
266        assert_eq!(combined.blend_mode, BlendMode::SrcOver);
267        assert!(combined.render_effect.is_none());
268        assert!(combined.backdrop_effect.is_none());
269    }
270
271    #[test]
272    fn combine_layers_uses_local_effect_configuration() {
273        let parent = GraphicsLayer {
274            render_effect: Some(RenderEffect::blur(8.0)),
275            ..Default::default()
276        };
277        let local = GraphicsLayer {
278            render_effect: Some(RenderEffect::offset(5.0, 1.0)),
279            backdrop_effect: Some(RenderEffect::blur(1.0)),
280            ..Default::default()
281        };
282
283        let combined = combine_layers(parent, Some(local.clone()));
284        assert_eq!(combined.render_effect, local.render_effect);
285        assert_eq!(combined.backdrop_effect, local.backdrop_effect);
286    }
287
288    #[test]
289    fn combine_layers_composes_color_filters_in_order() {
290        let parent_filter = ColorFilter::modulate(Color::from_rgba_u8(255, 128, 128, 255));
291        let parent = GraphicsLayer {
292            color_filter: Some(parent_filter),
293            ..Default::default()
294        };
295        let local_filter = ColorFilter::tint(Color::from_rgba_u8(128, 255, 64, 128));
296        let local = GraphicsLayer {
297            color_filter: Some(local_filter),
298            ..Default::default()
299        };
300
301        let combined = combine_layers(parent, Some(local));
302        let filter = combined.color_filter.expect("composed filter");
303        let source = [0.8, 0.5, 0.2, 0.75];
304        let expected = local_filter.apply_rgba(parent_filter.apply_rgba(source));
305        let observed = filter.apply_rgba(source);
306        assert!((observed[0] - expected[0]).abs() < 1e-6);
307        assert!((observed[1] - expected[1]).abs() < 1e-6);
308        assert!((observed[2] - expected[2]).abs() < 1e-6);
309        assert!((observed[3] - expected[3]).abs() < 1e-6);
310    }
311
312    #[test]
313    fn combine_layers_multiplies_axis_scales() {
314        let parent = GraphicsLayer {
315            scale: 1.2,
316            scale_x: 1.1,
317            scale_y: 0.9,
318            ..Default::default()
319        };
320        let local = GraphicsLayer {
321            scale: 0.5,
322            scale_x: 0.8,
323            scale_y: 1.5,
324            ..Default::default()
325        };
326
327        let combined = combine_layers(parent, Some(local));
328        assert!((combined.scale - 0.6).abs() < 1e-6);
329        assert!((combined.scale_x - 0.88).abs() < 1e-6);
330        assert!((combined.scale_y - 1.35).abs() < 1e-6);
331    }
332
333    #[test]
334    fn combine_layers_merges_rotation_clip_shape_and_shadow() {
335        let parent = GraphicsLayer {
336            rotation_x: 1.0,
337            rotation_y: 2.0,
338            rotation_z: 3.0,
339            camera_distance: 8.0,
340            transform_origin: TransformOrigin::CENTER,
341            shadow_elevation: 0.0,
342            ambient_shadow_color: Color::BLACK,
343            spot_shadow_color: Color::BLACK,
344            shape: LayerShape::Rectangle,
345            clip: false,
346            ..Default::default()
347        };
348        let local = GraphicsLayer {
349            rotation_x: 4.0,
350            rotation_y: 5.0,
351            rotation_z: 6.0,
352            camera_distance: 12.0,
353            transform_origin: TransformOrigin::new(0.25, 0.75),
354            shadow_elevation: 7.0,
355            ambient_shadow_color: Color::from_rgba_u8(10, 20, 30, 255),
356            spot_shadow_color: Color::from_rgba_u8(40, 50, 60, 255),
357            shape: LayerShape::Rounded(RoundedCornerShape::uniform(8.0)),
358            clip: true,
359            ..Default::default()
360        };
361
362        let combined = combine_layers(parent, Some(local));
363        assert!((combined.rotation_x - 5.0).abs() < 1e-6);
364        assert!((combined.rotation_y - 7.0).abs() < 1e-6);
365        assert!((combined.rotation_z - 9.0).abs() < 1e-6);
366        assert!((combined.camera_distance - 12.0).abs() < 1e-6);
367        assert_eq!(combined.transform_origin, TransformOrigin::new(0.25, 0.75));
368        assert!((combined.shadow_elevation - 7.0).abs() < 1e-6);
369        assert_eq!(
370            combined.ambient_shadow_color,
371            Color::from_rgba_u8(10, 20, 30, 255)
372        );
373        assert_eq!(
374            combined.spot_shadow_color,
375            Color::from_rgba_u8(40, 50, 60, 255)
376        );
377        assert_eq!(
378            combined.shape,
379            LayerShape::Rounded(RoundedCornerShape::uniform(8.0))
380        );
381        assert!(combined.clip);
382    }
383
384    #[test]
385    fn combine_layers_local_defaults_reset_parent_local_fields() {
386        let parent = GraphicsLayer {
387            camera_distance: 24.0,
388            transform_origin: TransformOrigin::new(0.1, 0.9),
389            shadow_elevation: 6.0,
390            ambient_shadow_color: Color::from_rgba_u8(20, 40, 60, 255),
391            spot_shadow_color: Color::from_rgba_u8(80, 100, 120, 255),
392            shape: LayerShape::Rounded(RoundedCornerShape::uniform(9.0)),
393            compositing_strategy: CompositingStrategy::Offscreen,
394            blend_mode: BlendMode::DstOut,
395            ..Default::default()
396        };
397
398        let combined = combine_layers(parent, Some(GraphicsLayer::default()));
399
400        assert!((combined.camera_distance - 8.0).abs() < 1e-6);
401        assert_eq!(combined.transform_origin, TransformOrigin::CENTER);
402        assert!((combined.shadow_elevation - 0.0).abs() < 1e-6);
403        assert_eq!(combined.ambient_shadow_color, Color::BLACK);
404        assert_eq!(combined.spot_shadow_color, Color::BLACK);
405        assert_eq!(combined.shape, LayerShape::Rectangle);
406        assert_eq!(combined.compositing_strategy, CompositingStrategy::Auto);
407        assert_eq!(combined.blend_mode, BlendMode::SrcOver);
408    }
409
410    #[test]
411    fn apply_draw_commands_scales_round_rect_radii_with_uniform_axis_scale() {
412        let command = DrawCommand::Behind(std::rc::Rc::new(|_size| {
413            vec![DrawPrimitive::RoundRect {
414                rect: Rect {
415                    x: 0.0,
416                    y: 0.0,
417                    width: 80.0,
418                    height: 40.0,
419                },
420                brush: Brush::solid(Color::BLACK),
421                radii: CornerRadii::uniform(10.0),
422            }]
423        }));
424
425        let layer = GraphicsLayer {
426            scale: 1.0,
427            scale_x: 2.0,
428            scale_y: 0.5,
429            ..Default::default()
430        };
431        let mut scene = RasterScene::new();
432        let bounds = Rect {
433            x: 0.0,
434            y: 0.0,
435            width: 80.0,
436            height: 40.0,
437        };
438        apply_draw_commands(
439            &[command],
440            DrawPlacement::Behind,
441            bounds,
442            Size {
443                width: 80.0,
444                height: 40.0,
445            },
446            &layer,
447            None,
448            &mut scene,
449        );
450
451        let shape = scene.shapes[0].shape.expect("rounded shape");
452        let radii = shape.radii();
453        assert!((radii.top_left - 5.0).abs() < 1e-6);
454        assert!((radii.top_right - 5.0).abs() < 1e-6);
455        assert!((radii.bottom_right - 5.0).abs() < 1e-6);
456        assert!((radii.bottom_left - 5.0).abs() < 1e-6);
457    }
458
459    #[test]
460    fn primitives_for_placement_uses_last_content_marker() {
461        let command = DrawCommand::WithContent(std::rc::Rc::new(|_size| {
462            vec![
463                DrawPrimitive::Rect {
464                    rect: Rect {
465                        x: 0.0,
466                        y: 0.0,
467                        width: 10.0,
468                        height: 10.0,
469                    },
470                    brush: Brush::solid(Color::from_rgba_u8(255, 0, 0, 255)),
471                },
472                DrawPrimitive::Content,
473                DrawPrimitive::Rect {
474                    rect: Rect {
475                        x: 0.0,
476                        y: 0.0,
477                        width: 10.0,
478                        height: 10.0,
479                    },
480                    brush: Brush::solid(Color::from_rgba_u8(0, 255, 0, 255)),
481                },
482                DrawPrimitive::Content,
483                DrawPrimitive::Rect {
484                    rect: Rect {
485                        x: 0.0,
486                        y: 0.0,
487                        width: 10.0,
488                        height: 10.0,
489                    },
490                    brush: Brush::solid(Color::from_rgba_u8(0, 0, 255, 255)),
491                },
492            ]
493        }));
494
495        let behind = primitives_for_placement(
496            &command,
497            DrawPlacement::Behind,
498            Size {
499                width: 10.0,
500                height: 10.0,
501            },
502        );
503        let overlay = primitives_for_placement(
504            &command,
505            DrawPlacement::Overlay,
506            Size {
507                width: 10.0,
508                height: 10.0,
509            },
510        );
511
512        assert_eq!(behind.len(), 2);
513        assert_eq!(overlay.len(), 1);
514    }
515}