use std::fmt;
use std::sync::Arc;
use crate::composition::Surface;
use crate::geometry::{
Clip, Color, Matrix4, Offset, RRect, Rect, rrect_radii_elliptical, transform3,
};
use crate::painting::{
Backdrop, BlendMode, Canvas, ClipOp, FillRule, ImageFilter, Paint, Path, Picture, valo,
};
#[derive(Debug, PartialEq)]
pub struct Scene {
pub picture: Arc<Picture>,
pub surfaces: Vec<SurfacePlacement>,
}
#[derive(Clone)]
pub struct SurfacePlacement {
pub surface: Arc<dyn Surface>,
pub rect: Rect,
pub scale: f64,
pub clip: Rect,
}
impl PartialEq for SurfacePlacement {
fn eq(&self, other: &SurfacePlacement) -> bool {
Arc::ptr_eq(&self.surface, &other.surface)
&& self.rect == other.rect
&& self.scale == other.scale
&& self.clip == other.clip
}
}
impl fmt::Debug for SurfacePlacement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SurfacePlacement")
.field("surface", &Arc::as_ptr(&self.surface).cast::<()>())
.field("rect", &self.rect)
.field("scale", &self.scale)
.field("clip", &self.clip)
.finish()
}
}
pub type EngineLayer = Arc<Picture>;
pub struct SceneBuilder {
canvas: Canvas,
transform: Matrix4,
clip: Option<Rect>,
enclosing: Vec<EnclosingScope>,
surfaces: Vec<SurfacePlacement>,
}
struct EnclosingScope {
canvas: Canvas,
transform: Matrix4,
clip: Option<Rect>,
}
impl fmt::Debug for SceneBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SceneBuilder")
.field("open_scopes", &self.enclosing.len())
.finish_non_exhaustive()
}
}
impl Default for SceneBuilder {
fn default() -> SceneBuilder {
SceneBuilder::new()
}
}
impl SceneBuilder {
pub fn new() -> SceneBuilder {
SceneBuilder {
canvas: Canvas::new(),
transform: Matrix4::IDENTITY,
clip: None,
enclosing: Vec::new(),
surfaces: Vec::new(),
}
}
pub fn push_transform(&mut self, matrix4: &Matrix4) {
self.open(|canvas| {
canvas.save();
canvas.concat(matrix4);
});
self.transformed_by(matrix4);
}
pub fn push_offset(&mut self, dx: f64, dy: f64) {
self.open(|canvas| {
canvas.save();
canvas.translate(dx as f32, dy as f32);
});
self.transformed_by(&Matrix4::translation(dx as f32, dy as f32));
}
pub fn push_clip_rect(&mut self, rect: Rect, clip_behavior: Clip) {
self.open(|canvas| {
open_clip(canvas, clip_behavior, rect.into());
canvas.clip_rect(rect, ClipOp::Intersect);
});
self.clipped_to(rect);
}
pub fn push_clip_rrect(&mut self, rrect: RRect, clip_behavior: Clip) {
self.open(|canvas| {
open_clip(canvas, clip_behavior, rrect.outer_rect().into());
canvas.clip_rrect_radii_elliptical(
rrect.outer_rect(),
rrect_radii_elliptical(rrect),
ClipOp::Intersect,
);
});
self.clipped_to(rrect.outer_rect());
}
pub fn push_clip_path(&mut self, path: &Arc<Path>, clip_behavior: Clip) {
self.open(|canvas| {
open_clip(canvas, clip_behavior, path.bounds());
canvas.clip_path(path, FillRule::NonZero, ClipOp::Intersect);
});
self.clipped_to(bounds_of(path));
}
pub fn push_opacity(&mut self, alpha: i32, offset: Offset) {
self.open(|canvas| {
let paint = Paint {
color: Color::from_argb(alpha, 0, 0, 0).into(),
..Paint::default()
};
canvas.save_layer(None, &paint);
canvas.translate(offset.dx() as f32, offset.dy() as f32);
});
self.transformed_by(&Matrix4::translation(
offset.dx() as f32,
offset.dy() as f32,
));
}
pub fn push_backdrop_filter(
&mut self,
filter: &ImageFilter,
blend_mode: BlendMode,
backdrop_id: Option<u64>,
) {
let backdrop = valo_backdrop(filter, backdrop_id);
let paint = Paint {
blend_mode,
..Paint::default()
};
self.open(|canvas| canvas.save_layer_backdrop(None, &paint, backdrop));
}
pub fn pop(&mut self) -> EngineLayer {
let enclosing = self
.enclosing
.pop()
.expect("SceneBuilder::pop without a push");
self.canvas.restore();
let scope = std::mem::replace(&mut self.canvas, enclosing.canvas);
self.transform = enclosing.transform;
self.clip = enclosing.clip;
let layer = Arc::new(scope.build());
self.canvas.draw_display_list(&layer);
layer
}
pub fn add_surface(&mut self, surface: Arc<dyn Surface>, rect: Rect) {
let rect = self.in_view_coordinates(rect);
let clip = match self.clip {
Some(clip) => visible_part_of(rect, clip),
None => rect,
};
self.surfaces.push(SurfacePlacement {
surface,
rect,
scale: horizontal_scale_of(self.transform),
clip,
});
}
pub fn add_retained(&mut self, retained_layer: &EngineLayer) {
self.canvas.draw_display_list(retained_layer);
}
pub fn add_picture(
&mut self,
offset: Offset,
picture: &Arc<Picture>,
is_complex_hint: bool,
will_change_hint: bool,
) {
let _ = (is_complex_hint, will_change_hint);
self.canvas.save();
self.canvas
.translate(offset.dx() as f32, offset.dy() as f32);
self.canvas.draw_display_list(picture);
self.canvas.restore();
}
pub fn build(self) -> Scene {
debug_assert!(
self.enclosing.is_empty(),
"SceneBuilder::build with an open push"
);
Scene {
picture: Arc::new(self.canvas.build()),
surfaces: self.surfaces,
}
}
fn open(&mut self, push: impl FnOnce(&mut Canvas)) {
let enclosing = std::mem::replace(&mut self.canvas, Canvas::new());
self.enclosing.push(EnclosingScope {
canvas: enclosing,
transform: self.transform,
clip: self.clip,
});
push(&mut self.canvas);
}
fn transformed_by(&mut self, matrix: &Matrix4) {
self.transform = self.transform.then(matrix);
}
fn clipped_to(&mut self, bounds: Rect) {
let bounds = self.in_view_coordinates(bounds);
self.clip = Some(match self.clip {
Some(clip) => visible_part_of(bounds, clip),
None => bounds,
});
}
fn in_view_coordinates(&self, rect: Rect) -> Rect {
let corners = [
Offset::new(rect.left, rect.top),
Offset::new(rect.right, rect.top),
Offset::new(rect.left, rect.bottom),
Offset::new(rect.right, rect.bottom),
]
.map(|corner| transform3(self.transform, corner));
let xs = corners.iter().map(|corner| corner.dx());
let ys = corners.iter().map(|corner| corner.dy());
Rect::from_ltrb(
xs.clone().fold(f64::INFINITY, f64::min),
ys.clone().fold(f64::INFINITY, f64::min),
xs.fold(f64::NEG_INFINITY, f64::max),
ys.fold(f64::NEG_INFINITY, f64::max),
)
}
}
fn open_clip(canvas: &mut Canvas, clip_behavior: Clip, bounds: valo::Rect) {
match clip_behavior {
Clip::None | Clip::HardEdge | Clip::AntiAlias => canvas.save(),
Clip::AntiAliasWithSaveLayer => canvas.save_layer(Some(bounds), &Paint::default()),
}
}
fn bounds_of(path: &Path) -> Rect {
let bounds = path.bounds();
Rect::from_ltwh(
f64::from(bounds.x),
f64::from(bounds.y),
f64::from(bounds.width),
f64::from(bounds.height),
)
}
fn visible_part_of(rect: Rect, clip: Rect) -> Rect {
let overlap = rect.intersect(clip);
Rect::from_ltrb(
overlap.left,
overlap.top,
overlap.right.max(overlap.left),
overlap.bottom.max(overlap.top),
)
}
fn horizontal_scale_of(transform: Matrix4) -> f64 {
let m = transform.to_flutter_array();
f64::from(m[0]).hypot(f64::from(m[1]))
}
fn valo_backdrop(filter: &ImageFilter, backdrop_id: Option<u64>) -> Backdrop {
let backdrop = Backdrop::new(filter.to_valo());
match backdrop_id {
Some(id) => backdrop.shared(id),
None => backdrop,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::painting::{ColorFilter, flattened_ops};
#[test]
fn pushes_and_pops_balance() {
let mut builder = SceneBuilder::new();
builder.push_offset(1.0, 2.0);
builder.push_opacity(128, Offset::ZERO);
builder.pop();
builder.pop();
let _scene = builder.build();
}
#[test]
fn a_popped_scope_is_the_engine_layer_embedded_in_its_parent() {
let mut canvas = Canvas::new();
canvas.draw_rect(Rect::from_ltwh(0.0, 0.0, 10.0, 10.0), &Paint::default());
let picture = Arc::new(canvas.build());
let mut builder = SceneBuilder::new();
builder.push_offset(1.0, 2.0);
builder.add_picture(Offset::ZERO, &picture, false, false);
let layer = builder.pop();
builder.add_retained(&layer);
let scene = builder.build();
let embedded = scene
.picture
.ops()
.iter()
.filter(|op| matches!(op, valo::Op::DrawDisplayList { list, .. } if Arc::ptr_eq(list, &layer)))
.count();
assert_eq!(
embedded,
2,
"once from the pop, once retained: {:?}",
scene.picture.ops()
);
}
struct IdleSurface;
impl Surface for IdleSurface {
fn present(&self, buffer: crate::Buffer, _released: Box<dyn FnOnce() + Send + Sync>) {
drop(buffer);
}
fn next_vsync(&self, _observer: std::sync::Weak<dyn crate::VsyncObserver>) {}
}
fn idle_surface() -> Arc<dyn Surface> {
Arc::new(IdleSurface)
}
#[test]
fn a_surface_is_placed_under_the_open_transforms() {
let (inner, outer) = (idle_surface(), idle_surface());
let mut builder = SceneBuilder::new();
builder.push_transform(&Matrix4::scale(2.0, 2.0));
builder.push_offset(1.0, 1.0);
builder.add_surface(Arc::clone(&inner), Rect::from_ltwh(0.0, 0.0, 1.0, 1.0));
builder.pop();
builder.pop();
builder.add_surface(Arc::clone(&outer), Rect::from_ltwh(10.0, 20.0, 5.0, 5.0));
let scene = builder.build();
assert_eq!(
scene.surfaces,
[
unclipped(inner, Rect::from_ltrb(2.0, 2.0, 4.0, 4.0), 2.0),
unclipped(outer, Rect::from_ltwh(10.0, 20.0, 5.0, 5.0), 1.0),
]
);
}
#[test]
fn a_surface_carries_the_scale_of_the_transforms_above_it() {
let surface = idle_surface();
let mut builder = SceneBuilder::new();
builder.push_transform(&Matrix4::scale(2.0, 2.0));
builder.push_offset(10.0, 20.0);
builder.push_transform(&Matrix4::scale(0.5, 0.5));
builder.add_surface(
Arc::clone(&surface),
Rect::from_ltwh(0.0, 0.0, 390.0, 844.0),
);
builder.pop();
builder.pop();
builder.pop();
let placement = &builder.build().surfaces[0];
assert_eq!(placement.scale, 1.0);
assert_eq!(placement.rect, Rect::from_ltwh(20.0, 40.0, 390.0, 844.0));
}
#[test]
fn a_surface_shows_only_what_the_clips_above_it_leave() {
let (clipped, beside) = (idle_surface(), idle_surface());
let mut builder = SceneBuilder::new();
builder.push_transform(&Matrix4::scale(2.0, 2.0));
builder.push_clip_rect(Rect::from_ltwh(0.0, 0.0, 100.0, 50.0), Clip::HardEdge);
builder.push_offset(10.0, 10.0);
let path = {
let mut path = crate::painting::PathBuilder::new();
path.rrect(Rect::from_ltwh(0.0, 0.0, 200.0, 20.0).into(), 4.0);
path.build()
};
builder.push_clip_path(&path, Clip::AntiAlias);
builder.add_surface(
Arc::clone(&clipped),
Rect::from_ltwh(-5.0, -5.0, 300.0, 300.0),
);
builder.pop();
builder.pop();
builder.add_surface(Arc::clone(&beside), Rect::from_ltwh(0.0, 0.0, 300.0, 300.0));
builder.pop();
builder.pop();
let scene = builder.build();
assert_eq!(
scene.surfaces[0].clip,
Rect::from_ltrb(20.0, 20.0, 200.0, 60.0)
);
assert_eq!(
scene.surfaces[0].rect,
Rect::from_ltwh(10.0, 10.0, 600.0, 600.0)
);
assert_eq!(
scene.surfaces[1].clip,
Rect::from_ltwh(0.0, 0.0, 200.0, 100.0),
"the rectangle's clip alone, once the path's scope is closed"
);
}
#[test]
fn a_surface_outside_its_clip_shows_nothing() {
let surface = idle_surface();
let mut builder = SceneBuilder::new();
builder.push_clip_rect(Rect::from_ltwh(0.0, 0.0, 100.0, 100.0), Clip::HardEdge);
builder.add_surface(surface, Rect::from_ltwh(200.0, 0.0, 50.0, 50.0));
builder.pop();
let clip = builder.build().surfaces[0].clip;
assert!(clip.is_empty(), "{clip:?}");
}
#[test]
fn a_placement_is_its_surface_its_rectangle_its_scale_and_what_shows() {
let surface = idle_surface();
let rect = Rect::from_ltwh(0.0, 0.0, 4.0, 4.0);
let placement = unclipped(Arc::clone(&surface), rect, 1.0);
assert_eq!(placement.clone(), placement);
let elsewhere = unclipped(
Arc::clone(&surface),
Rect::from_ltwh(1.0, 0.0, 4.0, 4.0),
1.0,
);
assert_ne!(elsewhere, placement);
let smaller = unclipped(Arc::clone(&surface), rect, 0.5);
assert_ne!(smaller, placement);
let cut = SurfacePlacement {
clip: Rect::from_ltwh(0.0, 0.0, 2.0, 4.0),
..placement.clone()
};
assert_ne!(cut, placement);
let another_surface = unclipped(idle_surface(), rect, 1.0);
assert_ne!(
another_surface, placement,
"the same rectangle is not the same surface"
);
}
fn unclipped(surface: Arc<dyn Surface>, rect: Rect, scale: f64) -> SurfacePlacement {
SurfacePlacement {
surface,
rect,
scale,
clip: rect,
}
}
#[test]
fn a_colour_filter_composed_over_a_blur_reaches_the_backdrop_in_its_order() {
let saturation = ColorFilter::saturation(1.8);
let filter =
ImageFilter::compose(ImageFilter::Color(saturation), ImageFilter::blur(4.0, 4.0));
let backdrop = recorded_backdrop(&filter, Some(7));
assert_eq!(
backdrop.filter,
Some(valo::ImageFilter::compose(
valo::ImageFilter::Color(saturation.to_valo()),
valo::ImageFilter::blur(4.0, 4.0),
))
);
assert_eq!(backdrop.key, Some(7), "the backdrop is still shared");
}
#[test]
fn an_uneven_blur_reaches_the_backdrop_with_both_sigmas() {
let backdrop = recorded_backdrop(&ImageFilter::blur(2.0, 6.0), None);
assert_eq!(backdrop.filter, Some(valo::ImageFilter::blur(2.0, 6.0)));
assert_eq!(backdrop.key, None);
}
#[test]
fn a_bounded_blur_reaches_the_backdrop_with_its_bounds_and_leaves_the_layer_unconfined() {
let glass = Rect::from_ltwh(8.0, 8.0, 16.0, 16.0);
let bounded = recorded_backdrop(&ImageFilter::blur(2.0, 3.0).bounds(glass), None);
assert_eq!(
bounded.filter,
Some(valo::ImageFilter::bounded_blur(2.0, 3.0, glass.into()))
);
let unbounded = recorded_backdrop(&ImageFilter::blur(2.0, 3.0), None);
assert_eq!(
bounded.bounds, unbounded.bounds,
"the bounds limit what the blur reads, not the layer"
);
}
struct RecordedBackdrop {
filter: Option<valo::ImageFilter>,
key: Option<u64>,
bounds: valo::Bounds,
}
fn recorded_backdrop(filter: &ImageFilter, backdrop_id: Option<u64>) -> RecordedBackdrop {
let mut builder = SceneBuilder::new();
builder.push_backdrop_filter(filter, BlendMode::SrcOver, backdrop_id);
builder.pop();
flattened_ops(&builder.build().picture)
.into_iter()
.find_map(|op| match op {
valo::Op::SaveLayer {
backdrop,
scope_bounds,
..
} => Some(RecordedBackdrop {
key: backdrop.as_ref().and_then(|backdrop| backdrop.shared_key),
filter: backdrop.map(|backdrop| backdrop.filter),
bounds: scope_bounds,
}),
_ => None,
})
.expect("a backdrop filter opens a layer")
}
#[cfg(feature = "test-support")]
mod drawn {
use super::*;
use crate::test_support::drawn_pixels;
const SIZE: u32 = 32;
#[test]
fn a_colour_filter_composed_over_a_blur_recolours_the_backdrop() {
let swap_red_and_green = ColorFilter::matrix([
0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ]);
let glass = Rect::from_ltwh(8.0, 8.0, 16.0, 16.0);
let filter = ImageFilter::compose(
ImageFilter::Color(swap_red_and_green),
ImageFilter::blur(2.0, 2.0).bounds(glass),
);
let mut builder = SceneBuilder::new();
builder.add_picture(Offset::ZERO, &filled(Color::new(0xFFFF0000)), false, false);
builder.push_clip_rect(glass, Clip::HardEdge);
builder.push_backdrop_filter(&filter, BlendMode::SrcOver, None);
builder.pop();
builder.pop();
let pixels = drawn_pixels(&builder.build().picture, [SIZE, SIZE]);
assert_near(
pixel_at(&pixels, 16, 16),
[0, 255, 0, 255],
"inside the glass",
);
assert_near(
pixel_at(&pixels, 2, 2),
[255, 0, 0, 255],
"outside the glass",
);
}
fn filled(color: Color) -> Arc<Picture> {
let mut canvas = Canvas::new();
let paint = Paint {
color: color.into(),
..Paint::default()
};
canvas.draw_rect(Rect::from_ltwh(0.0, 0.0, SIZE.into(), SIZE.into()), &paint);
Arc::new(canvas.build())
}
fn pixel_at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] {
let start = (y * SIZE as usize + x) * 4;
pixels[start..start + 4]
.try_into()
.expect("four bytes a pixel")
}
fn assert_near(actual: [u8; 4], expected: [u8; 4], place: &str) {
let near = actual
.iter()
.zip(expected)
.all(|(actual, expected)| actual.abs_diff(expected) <= 2);
assert!(near, "{place}: {actual:?}, expected {expected:?}");
}
}
#[test]
fn a_scene_without_surfaces_places_none() {
let scene = SceneBuilder::new().build();
assert!(scene.surfaces.is_empty());
assert!(scene.picture.ops().is_empty());
}
#[test]
#[should_panic(expected = "open push")]
fn build_with_an_open_push_panics() {
let mut builder = SceneBuilder::new();
builder.push_offset(0.0, 0.0);
let _scene = builder.build();
}
#[test]
#[should_panic(expected = "without a push")]
fn pop_without_a_push_panics() {
SceneBuilder::new().pop();
}
}