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