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                    cutout,
133                    blur_radius: _,
134                    blend_mode: shadow_blend_mode,
135                } => {
136                    // Pixels renderer currently ignores blur radius and renders the base
137                    // shadow geometry directly.
138                    emit_primitive(
139                        *shape,
140                        layer_bounds,
141                        layer,
142                        clip,
143                        scene,
144                        blend_mode.or(Some(shadow_blend_mode)),
145                    );
146                    if let Some(cutout) = cutout {
147                        emit_primitive(
148                            *cutout,
149                            layer_bounds,
150                            layer,
151                            clip,
152                            scene,
153                            Some(BlendMode::DstOut),
154                        );
155                    }
156                }
157                ShadowPrimitive::Inner {
158                    fill,
159                    cutout,
160                    blur_radius: _,
161                    blend_mode: shadow_blend_mode,
162                    clip_rect,
163                } => {
164                    let abs_clip = Rect {
165                        x: clip_rect.x + layer_bounds.x,
166                        y: clip_rect.y + layer_bounds.y,
167                        width: clip_rect.width,
168                        height: clip_rect.height,
169                    };
170                    let transformed_clip = apply_layer_to_rect(abs_clip, layer_bounds, layer);
171                    let shadow_clip = clip.map_or(Some(transformed_clip), |parent_clip| {
172                        parent_clip.intersect(transformed_clip)
173                    });
174                    emit_primitive(
175                        *fill,
176                        layer_bounds,
177                        layer,
178                        shadow_clip,
179                        scene,
180                        blend_mode.or(Some(shadow_blend_mode)),
181                    );
182                    emit_primitive(
183                        *cutout,
184                        layer_bounds,
185                        layer,
186                        shadow_clip,
187                        scene,
188                        blend_mode.or(Some(BlendMode::DstOut)),
189                    );
190                }
191            },
192        }
193    }
194
195    for command in commands {
196        let primitives = primitives_for_placement(command, placement, size);
197        for primitive in primitives {
198            emit_primitive(primitive, rect, layer, clip, scene, None);
199        }
200    }
201}
202
203pub(crate) fn point_in_resolved_rounded_rect(
204    x: f32,
205    y: f32,
206    rect: Rect,
207    radii: &CornerRadii,
208) -> bool {
209    if !rect.contains(x, y) {
210        return false;
211    }
212    let left = rect.x;
213    let right = rect.x + rect.width;
214    let top = rect.y;
215    let bottom = rect.y + rect.height;
216
217    if radii.top_left > 0.0 && x < left + radii.top_left && y < top + radii.top_left {
218        let cx = left + radii.top_left;
219        let cy = top + radii.top_left;
220        if (x - cx).powi(2) + (y - cy).powi(2) > radii.top_left.powi(2) {
221            return false;
222        }
223    }
224    if radii.top_right > 0.0 && x > right - radii.top_right && y < top + radii.top_right {
225        let cx = right - radii.top_right;
226        let cy = top + radii.top_right;
227        if (x - cx).powi(2) + (y - cy).powi(2) > radii.top_right.powi(2) {
228            return false;
229        }
230    }
231    if radii.bottom_right > 0.0 && x > right - radii.bottom_right && y > bottom - radii.bottom_right
232    {
233        let cx = right - radii.bottom_right;
234        let cy = bottom - radii.bottom_right;
235        if (x - cx).powi(2) + (y - cy).powi(2) > radii.bottom_right.powi(2) {
236            return false;
237        }
238    }
239    if radii.bottom_left > 0.0 && x < left + radii.bottom_left && y > bottom - radii.bottom_left {
240        let cx = left + radii.bottom_left;
241        let cy = bottom - radii.bottom_left;
242        if (x - cx).powi(2) + (y - cy).powi(2) > radii.bottom_left.powi(2) {
243            return false;
244        }
245    }
246    true
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use cranpose_ui::Brush;
253    use cranpose_ui_graphics::{
254        Color, ColorFilter, CompositingStrategy, LayerShape, RenderEffect, RoundedCornerShape,
255        TransformOrigin,
256    };
257
258    #[test]
259    fn combine_layers_clears_effects_without_new_layer() {
260        let current = GraphicsLayer {
261            alpha: 0.7,
262            scale: 1.2,
263            translation_x: 4.0,
264            translation_y: 6.0,
265            color_filter: None,
266            render_effect: Some(RenderEffect::blur(4.0)),
267            backdrop_effect: Some(RenderEffect::blur(2.0)),
268            ..Default::default()
269        };
270
271        let combined = combine_layers(current.clone(), None);
272        assert_eq!(combined.alpha, current.alpha);
273        assert_eq!(combined.scale, current.scale);
274        assert_eq!(combined.translation_x, current.translation_x);
275        assert_eq!(combined.translation_y, current.translation_y);
276        assert_eq!(combined.compositing_strategy, CompositingStrategy::Auto);
277        assert_eq!(combined.blend_mode, BlendMode::SrcOver);
278        assert!(combined.render_effect.is_none());
279        assert!(combined.backdrop_effect.is_none());
280    }
281
282    #[test]
283    fn combine_layers_uses_local_effect_configuration() {
284        let parent = GraphicsLayer {
285            render_effect: Some(RenderEffect::blur(8.0)),
286            ..Default::default()
287        };
288        let local = GraphicsLayer {
289            render_effect: Some(RenderEffect::offset(5.0, 1.0)),
290            backdrop_effect: Some(RenderEffect::blur(1.0)),
291            ..Default::default()
292        };
293
294        let combined = combine_layers(parent, Some(local.clone()));
295        assert_eq!(combined.render_effect, local.render_effect);
296        assert_eq!(combined.backdrop_effect, local.backdrop_effect);
297    }
298
299    #[test]
300    fn combine_layers_composes_color_filters_in_order() {
301        let parent_filter = ColorFilter::modulate(Color::from_rgba_u8(255, 128, 128, 255));
302        let parent = GraphicsLayer {
303            color_filter: Some(parent_filter),
304            ..Default::default()
305        };
306        let local_filter = ColorFilter::tint(Color::from_rgba_u8(128, 255, 64, 128));
307        let local = GraphicsLayer {
308            color_filter: Some(local_filter),
309            ..Default::default()
310        };
311
312        let combined = combine_layers(parent, Some(local));
313        let filter = combined.color_filter.expect("composed filter");
314        let source = [0.8, 0.5, 0.2, 0.75];
315        let expected = local_filter.apply_rgba(parent_filter.apply_rgba(source));
316        let observed = filter.apply_rgba(source);
317        assert!((observed[0] - expected[0]).abs() < 1e-6);
318        assert!((observed[1] - expected[1]).abs() < 1e-6);
319        assert!((observed[2] - expected[2]).abs() < 1e-6);
320        assert!((observed[3] - expected[3]).abs() < 1e-6);
321    }
322
323    #[test]
324    fn combine_layers_multiplies_axis_scales() {
325        let parent = GraphicsLayer {
326            scale: 1.2,
327            scale_x: 1.1,
328            scale_y: 0.9,
329            ..Default::default()
330        };
331        let local = GraphicsLayer {
332            scale: 0.5,
333            scale_x: 0.8,
334            scale_y: 1.5,
335            ..Default::default()
336        };
337
338        let combined = combine_layers(parent, Some(local));
339        assert!((combined.scale - 0.6).abs() < 1e-6);
340        assert!((combined.scale_x - 0.88).abs() < 1e-6);
341        assert!((combined.scale_y - 1.35).abs() < 1e-6);
342    }
343
344    #[test]
345    fn combine_layers_merges_rotation_clip_shape_and_shadow() {
346        let parent = GraphicsLayer {
347            rotation_x: 1.0,
348            rotation_y: 2.0,
349            rotation_z: 3.0,
350            camera_distance: 8.0,
351            transform_origin: TransformOrigin::CENTER,
352            shadow_elevation: 0.0,
353            ambient_shadow_color: Color::BLACK,
354            spot_shadow_color: Color::BLACK,
355            shape: LayerShape::Rectangle,
356            clip: false,
357            ..Default::default()
358        };
359        let local = GraphicsLayer {
360            rotation_x: 4.0,
361            rotation_y: 5.0,
362            rotation_z: 6.0,
363            camera_distance: 12.0,
364            transform_origin: TransformOrigin::new(0.25, 0.75),
365            shadow_elevation: 7.0,
366            ambient_shadow_color: Color::from_rgba_u8(10, 20, 30, 255),
367            spot_shadow_color: Color::from_rgba_u8(40, 50, 60, 255),
368            shape: LayerShape::Rounded(RoundedCornerShape::uniform(8.0)),
369            clip: true,
370            ..Default::default()
371        };
372
373        let combined = combine_layers(parent, Some(local));
374        assert!((combined.rotation_x - 5.0).abs() < 1e-6);
375        assert!((combined.rotation_y - 7.0).abs() < 1e-6);
376        assert!((combined.rotation_z - 9.0).abs() < 1e-6);
377        assert!((combined.camera_distance - 12.0).abs() < 1e-6);
378        assert_eq!(combined.transform_origin, TransformOrigin::new(0.25, 0.75));
379        assert!((combined.shadow_elevation - 7.0).abs() < 1e-6);
380        assert_eq!(
381            combined.ambient_shadow_color,
382            Color::from_rgba_u8(10, 20, 30, 255)
383        );
384        assert_eq!(
385            combined.spot_shadow_color,
386            Color::from_rgba_u8(40, 50, 60, 255)
387        );
388        assert_eq!(
389            combined.shape,
390            LayerShape::Rounded(RoundedCornerShape::uniform(8.0))
391        );
392        assert!(combined.clip);
393    }
394
395    #[test]
396    fn combine_layers_local_defaults_reset_parent_local_fields() {
397        let parent = GraphicsLayer {
398            camera_distance: 24.0,
399            transform_origin: TransformOrigin::new(0.1, 0.9),
400            shadow_elevation: 6.0,
401            ambient_shadow_color: Color::from_rgba_u8(20, 40, 60, 255),
402            spot_shadow_color: Color::from_rgba_u8(80, 100, 120, 255),
403            shape: LayerShape::Rounded(RoundedCornerShape::uniform(9.0)),
404            compositing_strategy: CompositingStrategy::Offscreen,
405            blend_mode: BlendMode::DstOut,
406            ..Default::default()
407        };
408
409        let combined = combine_layers(parent, Some(GraphicsLayer::default()));
410
411        assert!((combined.camera_distance - 8.0).abs() < 1e-6);
412        assert_eq!(combined.transform_origin, TransformOrigin::CENTER);
413        assert!((combined.shadow_elevation - 0.0).abs() < 1e-6);
414        assert_eq!(combined.ambient_shadow_color, Color::BLACK);
415        assert_eq!(combined.spot_shadow_color, Color::BLACK);
416        assert_eq!(combined.shape, LayerShape::Rectangle);
417        assert_eq!(combined.compositing_strategy, CompositingStrategy::Auto);
418        assert_eq!(combined.blend_mode, BlendMode::SrcOver);
419    }
420
421    #[test]
422    fn apply_draw_commands_scales_round_rect_radii_with_uniform_axis_scale() {
423        let command = DrawCommand::Behind(std::rc::Rc::new(|_size| {
424            vec![DrawPrimitive::RoundRect {
425                rect: Rect {
426                    x: 0.0,
427                    y: 0.0,
428                    width: 80.0,
429                    height: 40.0,
430                },
431                brush: Brush::solid(Color::BLACK),
432                radii: CornerRadii::uniform(10.0),
433            }]
434        }));
435
436        let layer = GraphicsLayer {
437            scale: 1.0,
438            scale_x: 2.0,
439            scale_y: 0.5,
440            ..Default::default()
441        };
442        let mut scene = RasterScene::new();
443        let bounds = Rect {
444            x: 0.0,
445            y: 0.0,
446            width: 80.0,
447            height: 40.0,
448        };
449        apply_draw_commands(
450            &[command],
451            DrawPlacement::Behind,
452            bounds,
453            Size {
454                width: 80.0,
455                height: 40.0,
456            },
457            &layer,
458            None,
459            &mut scene,
460        );
461
462        let shape = scene.shapes[0].shape.expect("rounded shape");
463        let radii = shape.radii();
464        assert!((radii.top_left - 5.0).abs() < 1e-6);
465        assert!((radii.top_right - 5.0).abs() < 1e-6);
466        assert!((radii.bottom_right - 5.0).abs() < 1e-6);
467        assert!((radii.bottom_left - 5.0).abs() < 1e-6);
468    }
469
470    #[test]
471    fn primitives_for_placement_uses_last_content_marker() {
472        let command = DrawCommand::WithContent(std::rc::Rc::new(|_size| {
473            vec![
474                DrawPrimitive::Rect {
475                    rect: Rect {
476                        x: 0.0,
477                        y: 0.0,
478                        width: 10.0,
479                        height: 10.0,
480                    },
481                    brush: Brush::solid(Color::from_rgba_u8(255, 0, 0, 255)),
482                },
483                DrawPrimitive::Content,
484                DrawPrimitive::Rect {
485                    rect: Rect {
486                        x: 0.0,
487                        y: 0.0,
488                        width: 10.0,
489                        height: 10.0,
490                    },
491                    brush: Brush::solid(Color::from_rgba_u8(0, 255, 0, 255)),
492                },
493                DrawPrimitive::Content,
494                DrawPrimitive::Rect {
495                    rect: Rect {
496                        x: 0.0,
497                        y: 0.0,
498                        width: 10.0,
499                        height: 10.0,
500                    },
501                    brush: Brush::solid(Color::from_rgba_u8(0, 0, 255, 255)),
502                },
503            ]
504        }));
505
506        let behind = primitives_for_placement(
507            &command,
508            DrawPlacement::Behind,
509            Size {
510                width: 10.0,
511                height: 10.0,
512            },
513        );
514        let overlay = primitives_for_placement(
515            &command,
516            DrawPlacement::Overlay,
517            Size {
518                width: 10.0,
519                height: 10.0,
520            },
521        );
522
523        assert_eq!(behind.len(), 2);
524        assert_eq!(overlay.len(), 1);
525    }
526}