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)] pub(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 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}