use super::CompositorCanvas;
use crate::media::imaging::Bitmap;
use crate::media::Colors;
use crate::numerics::{Quaternion, Vector3};
use crate::platform::{AlphaFormat, LtrbRect, PixelFormats};
use crate::rendering::composition::server::ServerDropShadow;
use crate::rendering::composition::{
CompositionDropShadowSourcePolicy, CompositionVisual, ElementComposition, ICompositionSurface, LoadedImageSurface,
SpriteVisual,
};
use crate::rendering::testing::{DrawingLog, MockDrawingContextImpl};
use crate::{PixelSize, Vector, Vector3D};
use std::rc::Rc;
fn show(s: &CompositorCanvas, visual: &Rc<CompositionVisual>) {
ElementComposition::set_element_child_visual(&s.canvas, Some(visual.clone()));
s.run_jobs();
}
fn position(entries: &[String], entry: &str) -> usize {
entries.iter().position(|e| e == entry).unwrap_or_else(|| panic!("{entry} is drawn: {entries:?}"))
}
fn position_starting(entries: &[String], prefix: &str) -> usize {
entries.iter().position(|e| e.starts_with(prefix)).unwrap_or_else(|| panic!("{prefix} is drawn: {entries:?}"))
}
fn count_starting(entries: &[String], prefix: &str) -> usize {
entries.iter().filter(|e| e.starts_with(prefix)).count()
}
fn test_bitmap() -> Bitmap {
Bitmap::from_pixels(
PixelFormats::RGBA8888,
AlphaFormat::Premul,
&[0u8; 20 * 10 * 4],
PixelSize::new(20, 10),
Vector::new(96.0, 96.0),
20 * 4,
)
}
fn blue_sprite(s: &CompositorCanvas, width: f64, height: f64) -> SpriteVisual {
let sprite = s.compositor.create_sprite_visual();
sprite.set_size(Vector::new(width, height));
sprite.set_brush(Some((*s.compositor.create_color_brush_with_color(Colors::BLUE)).clone()));
sprite
}
fn render_with_effects(s: &CompositorCanvas, visual: &Rc<CompositionVisual>) -> Vec<String> {
let log = DrawingLog::new();
let mut context = MockDrawingContextImpl::new(log.clone());
context.supports_effects = true;
context.log_effects = true;
context.log_render_options = true;
s.server_object(visual.server()).render(&mut context, crate::platform::LtrbRect::INFINITE, None, true, false, false);
log.entries()
}
fn shadow_of_alpha(entries: &[String]) -> &[String] {
let blur = position_starting(entries, "PushEffect");
let layer = blur + position_starting(&entries[blur..], "PushLayer");
let options = layer + position_starting(&entries[layer..], "PushRenderOptions");
let pop_layer = options + position(&entries[options..], "PopLayer");
let pop_blur = pop_layer + position(&entries[pop_layer..], "PopEffect");
assert!(entries[blur].contains("blur("), "the shadow is blurred: {entries:?}");
assert!(entries[options].contains("SourceIn"), "the colour is drawn with source-in blending: {entries:?}");
assert_eq!(
1,
count_starting(&entries[options..pop_layer], "DrawBitmap 1 1, 1, 2, 2"),
"the bitmap of the colour is drawn once over the source: {entries:?}"
);
assert!(!entries.iter().any(|e| e.contains("DestinationOut")), "nothing is removed: {entries:?}");
&entries[blur..=pop_blur]
}
#[test]
fn a_masked_shadow_is_the_alpha_of_its_mask_coloured_moved_and_blurred() {
let s = CompositorCanvas::new();
let log = s.render_interface.log().clone();
let image = LoadedImageSurface::from_bitmap(&s.compositor, &test_bitmap());
let surface: Rc<dyn ICompositionSurface> = image.clone();
let mask = s.compositor.create_surface_brush_with_surface(surface);
let shadow = s.compositor.create_drop_shadow();
shadow.set_offset(Vector3D::new(5.0, 6.0, 0.0));
shadow.set_mask(Some((*mask).clone()));
assert!(shadow.mask().is_some());
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
let entries = log.entries();
let box_shadow = position(&entries, "DrawRectangle none none 0, 0, 100, 50 shadows=1");
let brush = position(&entries, "DrawRectangle Blue none 0, 0, 100, 50 shadows=0");
assert!(box_shadow < brush, "{entries:?}");
let entries = render_with_effects(&s, &sprite);
let shadow_entries = shadow_of_alpha(&entries);
assert_eq!("PushEffect -4, -3, 118, 68 blur(9)", shadow_entries[0], "{entries:?}");
assert!(shadow_entries.iter().any(|e| e == "SetTransform { {M11:1 M12:0} {M21:0 M22:1} {M31:5 M32:6} }"), "{entries:?}");
assert_eq!(2, count_starting(shadow_entries, "DrawBitmap"), "{entries:?}");
let brush = position(&entries, "DrawRectangle Blue none 0, 0, 100, 50 shadows=0");
assert!(brush > position(&entries, "PopEffect"), "{entries:?}");
assert!(!entries.iter().any(|e| e.ends_with("shadows=1")), "{entries:?}");
log.clear();
mask.set_offset(Vector::new(5.0, 0.0));
s.run_jobs();
assert!(log.count("DrawRectangle Blue none 0, 0, 100, 50") > 0, "{:?}", log.entries());
}
#[test]
fn a_color_mask_casts_the_shadow_of_its_alpha_too() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_mask(Some((*s.compositor.create_color_brush_with_color(Colors::BLACK)).clone()));
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
let entries = render_with_effects(&s, &sprite);
let shadow_entries = shadow_of_alpha(&entries);
assert!(shadow_entries.iter().any(|e| e == "DrawRectangle Black none 0, 0, 100, 50 shadows=0"), "{entries:?}");
assert!(!entries.iter().any(|e| e.starts_with("PushOpacityMask")), "{entries:?}");
shadow.set_source_policy(CompositionDropShadowSourcePolicy::InheritFromVisualContent);
s.run_jobs();
let entries = render_with_effects(&s, &sprite);
let shadow_entries = shadow_of_alpha(&entries);
assert!(shadow_entries.iter().any(|e| e == "DrawRectangle Blue none 0, 0, 100, 50 shadows=0"), "{entries:?}");
assert!(!shadow_entries.iter().any(|e| e.starts_with("DrawRectangle Black")), "{entries:?}");
}
#[test]
fn a_shadow_that_inherits_from_the_content_is_the_alpha_of_the_brush_under_the_brush() {
let s = CompositorCanvas::new();
let log = s.render_interface.log().clone();
let shadow = s.compositor.create_drop_shadow();
shadow.set_source_policy(CompositionDropShadowSourcePolicy::InheritFromVisualContent);
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
assert!(
log.entries().iter().any(|e| e == "DrawRectangle none none 0, 0, 100, 50 shadows=1"),
"{:?}",
log.entries()
);
let entries = render_with_effects(&s, &sprite);
let shadow_entries = shadow_of_alpha(&entries);
assert_eq!(1, count_starting(shadow_entries, "DrawRectangle Blue"), "{entries:?}");
assert_eq!(2, count_starting(&entries, "DrawRectangle Blue"), "{entries:?}");
assert!(!entries.iter().any(|e| e.ends_with("shadows=1")), "{entries:?}");
sprite.set_brush(None);
s.run_jobs();
let entries = render_with_effects(&s, &sprite);
assert!(!entries.iter().any(|e| e.starts_with("PushEffect") || e.ends_with("shadows=1")), "{entries:?}");
}
#[test]
fn the_masked_shadow_of_a_layer_visual_is_drawn_under_its_layer() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_mask(Some((*s.compositor.create_color_brush_with_color(Colors::BLACK)).clone()));
let layer = s.compositor.create_layer_visual();
layer.set_size(Vector::new(100.0, 50.0));
layer.set_shadow(Some(shadow.clone()));
layer.children().insert_at_top((*blue_sprite(&s, 30.0, 20.0)).clone());
show(&s, &layer);
let entries = render_with_effects(&s, &layer);
let shadow_entries = shadow_of_alpha(&entries).len();
let blur = position_starting(&entries, "PushEffect");
let child = position(&entries, "DrawRectangle Blue none 0, 0, 30, 20 shadows=0");
assert!(blur + shadow_entries <= child, "{entries:?}");
assert_eq!(1, count_starting(&entries, "PushEffect"), "{entries:?}");
}
#[test]
fn an_invisible_shadow_draws_nothing() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_opacity(0.0);
shadow.set_mask(Some((*s.compositor.create_color_brush_with_color(Colors::BLACK)).clone()));
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
let entries = render_with_effects(&s, &sprite);
assert!(!entries.iter().any(|e| e.starts_with("PushEffect") || e.starts_with("PushLayer")), "{entries:?}");
assert_eq!(1, count_starting(&entries, "DrawRectangle"), "{entries:?}");
}
#[test]
fn the_color_of_the_shadow_carries_its_opacity() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_color(crate::media::Color::from_argb(200, 10, 20, 30));
shadow.set_opacity(0.5);
let sprite = blue_sprite(&s, 10.0, 10.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
let server = s.compositor.server().get::<ServerDropShadow>(shadow.server()).expect("the server shadow exists");
assert_eq!(crate::media::Color::from_argb(100, 10, 20, 30), server.effective_color());
}
#[test]
fn the_z_of_the_offset_moves_nothing_in_the_plane() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_offset(Vector3D::new(2.0, 3.0, 40.0));
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
show(&s, &sprite);
let server_shadow = s.compositor.server().get::<ServerDropShadow>(shadow.server()).expect("the server shadow exists");
let server = s.server_object(sprite.server());
assert_eq!(Vector::new(2.0, 3.0), server_shadow.plane_offset(Some(&server)));
assert_eq!(Some(LtrbRect::new(-7.0, -6.0, 111.0, 62.0)), server.sub_tree_bounds());
}
#[test]
fn the_z_of_the_offset_moves_the_shadow_of_a_visual_turned_out_of_its_plane() {
let s = CompositorCanvas::new();
let shadow = s.compositor.create_drop_shadow();
shadow.set_offset(Vector3D::new(0.0, 0.0, 10.0));
let sprite = blue_sprite(&s, 100.0, 50.0);
sprite.set_shadow(Some(shadow.clone()));
let angle = std::f32::consts::PI / 3.0;
sprite.set_orientation(Quaternion::create_from_axis_angle(Vector3::new(1.0, 0.0, 0.0), angle));
show(&s, &sprite);
let server_shadow = s.compositor.server().get::<ServerDropShadow>(shadow.server()).expect("the server shadow exists");
let server = s.server_object(sprite.server());
let offset = server_shadow.plane_offset(Some(&server));
assert!(offset.x.abs() < 1e-4, "{offset:?}");
assert!((offset.y - (-10.0 * (angle as f64).tan())).abs() < 1e-3, "{offset:?}");
let entries = render_with_effects(&s, &sprite);
let box_shadow = entries.iter().find(|e| e.ends_with("shadows=1"));
assert!(box_shadow.is_some(), "{entries:?}");
let bounds = server.compute_own_content_bounds().expect("the sprite has bounds");
assert!(bounds.top < -10.0 * (angle as f64).tan() - 8.0, "{bounds:?}");
shadow.set_offset(Vector3D::new(4.0, 0.0, 0.0));
s.run_jobs();
assert_eq!(Vector::new(4.0, 0.0), server_shadow.plane_offset(Some(&server)));
}