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 cutout,
133 blur_radius: _,
134 blend_mode: shadow_blend_mode,
135 } => {
136 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}