use std::fmt;
use std::sync::Arc;
use crate::geometry::{Clip, Color, Matrix4, Offset, RRect, Rect, rrect_radii_elliptical};
use crate::painting::{
Backdrop, BlendMode, Canvas, ClipOp, FillRule, ImageFilter, Paint, Path, Picture, valo,
};
pub type Scene = Picture;
pub type EngineLayer = Arc<Picture>;
pub struct SceneBuilder {
canvas: Canvas,
enclosing: Vec<Canvas>,
}
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(),
enclosing: Vec::new(),
}
}
pub fn push_transform(&mut self, matrix4: &Matrix4) {
self.open(|canvas| {
canvas.save();
canvas.concat(matrix4);
});
}
pub fn push_offset(&mut self, dx: f64, dy: f64) {
self.open(|canvas| {
canvas.save();
canvas.translate(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);
});
}
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,
);
});
}
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);
});
}
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);
});
}
pub fn push_backdrop_filter(
&mut self,
filter: &ImageFilter,
blend_mode: BlendMode,
backdrop_id: Option<u64>,
) {
let (sigma, bounds) = backdrop_blur(filter).unwrap_or((0.0, None));
let mut backdrop = Backdrop::blur(sigma as f32);
if let Some(id) = backdrop_id {
backdrop = backdrop.shared(id);
}
let paint = Paint {
blend_mode,
..Paint::default()
};
self.open(|canvas| {
canvas.save_layer_backdrop(bounds.map(Into::into), &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);
let layer = Arc::new(scope.build());
self.canvas.draw_display_list(&layer);
layer
}
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"
);
self.canvas.build()
}
fn open(&mut self, push: impl FnOnce(&mut Canvas)) {
let enclosing = std::mem::replace(&mut self.canvas, Canvas::new());
self.enclosing.push(enclosing);
push(&mut self.canvas);
}
}
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 backdrop_blur(filter: &ImageFilter) -> Option<(f64, Option<Rect>)> {
match filter {
ImageFilter::Blur {
sigma_x, bounds, ..
} => Some((*sigma_x, *bounds)),
ImageFilter::Color(_) => None,
ImageFilter::Compose { outer, inner } => {
backdrop_blur(inner).or_else(|| backdrop_blur(outer))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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
.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.ops()
);
}
#[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();
}
}