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