use crate::{Filter, Glyph, NormalizedCoord, PaintRef, PaintScene, RenderContext};
use kurbo::{Affine, BezPath, Rect, Shape, Stroke};
use peniko::{BlendMode, Color, Fill, FontData, StyleRef};
use std::sync::Arc;
fn overlaps(a: Rect, b: Rect) -> bool {
a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1
}
#[derive(Debug, Clone)]
pub struct BackdropOp {
pub filter: Arc<Filter>,
pub clip: BezPath,
pub bounds: Rect,
pub source: Rect,
}
#[derive(Debug, Clone, Default)]
pub struct BackdropBatch {
pub ops: Vec<BackdropOp>,
}
#[derive(Debug, Clone, Default)]
pub struct FramePlan {
pub batches: Vec<BackdropBatch>,
}
impl FramePlan {
pub fn render_passes(&self) -> u32 {
self.batches.len() as u32 + 1
}
pub fn blur_passes(&self) -> u32 {
self.batches
.iter()
.map(|batch| batch.ops.len() as u32)
.sum()
}
pub fn is_empty(&self) -> bool {
self.batches.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Boundary {
SameSegment,
NewSegment,
}
const MAX_OCCUPIED_REGIONS: usize = 16;
#[derive(Debug)]
struct OpenBatch {
depth: u32,
occupied: Vec<Rect>,
ops: Vec<BackdropOp>,
}
impl OpenBatch {
fn occupy(&mut self, bounds: Rect) {
if self.occupied.len() < MAX_OCCUPIED_REGIONS {
self.occupied.push(bounds);
return;
}
let collapsed = self
.occupied
.iter()
.copied()
.fold(bounds, |acc, rect| acc.union(rect));
self.occupied.clear();
self.occupied.push(collapsed);
}
fn is_clear(&self, region: Rect) -> bool {
!self
.occupied
.iter()
.any(|painted| overlaps(*painted, region))
}
}
#[derive(Debug, Default)]
pub struct BackdropPlanner {
depth: u32,
batches: Vec<BackdropBatch>,
open: Option<OpenBatch>,
}
impl BackdropPlanner {
pub fn new() -> Self {
Self::default()
}
pub fn is_tracking(&self) -> bool {
self.open.is_some()
}
pub fn push_layer(&mut self, bounds: Rect) {
if let Some(open) = &mut self.open {
if self.depth <= open.depth {
open.occupy(bounds);
}
}
self.depth += 1;
}
pub fn pop_layer(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
pub fn draw(&mut self, bounds: Rect) {
if let Some(open) = &mut self.open {
if self.depth <= open.depth {
open.occupy(bounds);
}
}
}
pub fn backdrop(&mut self, filter: Arc<Filter>, clip: BezPath, bounds: Rect) -> Boundary {
let expansion = filter.expansion_rect();
let source = Rect::new(
bounds.x0 + expansion.x0,
bounds.y0 + expansion.y0,
bounds.x1 + expansion.x1,
bounds.y1 + expansion.y1,
);
let op = BackdropOp {
filter,
clip,
bounds,
source,
};
let can_share = self.open.as_ref().is_some_and(|open| open.is_clear(source));
if can_share {
let open = self.open.as_mut().expect("checked above");
open.occupy(bounds);
open.ops.push(op);
return Boundary::SameSegment;
}
if let Some(previous) = self.open.take() {
self.batches.push(BackdropBatch { ops: previous.ops });
}
self.open = Some(OpenBatch {
depth: self.depth,
occupied: vec![bounds],
ops: vec![op],
});
Boundary::NewSegment
}
pub fn finish(mut self) -> FramePlan {
if let Some(open) = self.open.take() {
self.batches.push(BackdropBatch { ops: open.ops });
}
FramePlan {
batches: self.batches,
}
}
}
#[derive(Debug, Default)]
pub struct PlanningScene {
planner: BackdropPlanner,
}
impl PlanningScene {
pub fn new() -> Self {
Self::default()
}
pub fn finish(self) -> FramePlan {
self.planner.finish()
}
fn device_bounds(&self, transform: Affine, shape: &impl Shape) -> Option<Rect> {
self.planner
.is_tracking()
.then(|| transform.transform_rect_bbox(shape.bounding_box()))
}
}
impl RenderContext for PlanningScene {}
impl PaintScene for PlanningScene {
fn reset(&mut self) {
self.planner = BackdropPlanner::new();
}
fn push_layer(
&mut self,
_blend: impl Into<BlendMode>,
_alpha: f32,
transform: Affine,
clip: &impl Shape,
_filter: Option<Arc<Filter>>,
backdrop_filter: Option<Arc<Filter>>,
) {
let bounds = transform.transform_rect_bbox(clip.bounding_box());
if let Some(backdrop_filter) = backdrop_filter {
let clip = transform * clip.into_path(0.1);
self.planner.backdrop(backdrop_filter, clip, bounds);
}
self.planner.push_layer(bounds);
}
fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
let bounds = transform.transform_rect_bbox(clip.bounding_box());
self.planner.push_layer(bounds);
}
fn pop_layer(&mut self) {
self.planner.pop_layer();
}
fn stroke<'a>(
&mut self,
style: &Stroke,
transform: Affine,
_brush: impl Into<PaintRef<'a>>,
_brush_transform: Option<Affine>,
shape: &impl Shape,
) {
if let Some(bounds) = self.device_bounds(transform, shape) {
let half = style.width / 2.0;
self.planner.draw(bounds.inflate(half, half));
}
}
fn fill<'a>(
&mut self,
_style: Fill,
transform: Affine,
_brush: impl Into<PaintRef<'a>>,
_brush_transform: Option<Affine>,
shape: &impl Shape,
) {
if let Some(bounds) = self.device_bounds(transform, shape) {
self.planner.draw(bounds);
}
}
fn draw_glyphs<'a, 's: 'a>(
&'s mut self,
_font: &'a FontData,
font_size: f32,
_hint: bool,
_normalized_coords: &'a [NormalizedCoord],
_embolden: kurbo::Vec2,
_style: impl Into<StyleRef<'a>>,
_brush: impl Into<PaintRef<'a>>,
_brush_alpha: f32,
transform: Affine,
_glyph_transform: Option<Affine>,
glyphs: impl Iterator<Item = Glyph> + Clone,
) {
if !self.planner.is_tracking() {
return;
}
let size = f64::from(font_size);
let mut run: Option<Rect> = None;
for glyph in glyphs {
let x = f64::from(glyph.x);
let y = f64::from(glyph.y);
let cell = Rect::new(x, y - size, x + size, y + size / 3.0);
run = Some(match run {
Some(existing) => existing.union(cell),
None => cell,
});
}
if let Some(run) = run {
self.planner.draw(transform.transform_rect_bbox(run));
}
}
fn draw_box_shadow(
&mut self,
transform: Affine,
rect: Rect,
_brush: Color,
radius: f64,
std_dev: f64,
) {
if !self.planner.is_tracking() {
return;
}
let reach = std_dev * 3.0 + radius;
self.planner
.draw(transform.transform_rect_bbox(rect.inflate(reach, reach)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filters::FilterEffect;
use kurbo::Shape;
fn blur(std_dev: f32) -> Arc<Filter> {
Arc::new(Filter::single(FilterEffect::blur(std_dev)))
}
fn panel(x: f64, width: f64) -> (BezPath, Rect) {
let bounds = Rect::new(x, 0.0, x + width, 100.0);
(bounds.into_path(0.1), bounds)
}
fn plan_panels(count: usize, pitch: f64, width: f64, std_dev: f32) -> FramePlan {
let mut planner = BackdropPlanner::new();
for index in 0..count {
let (clip, bounds) = panel(index as f64 * pitch, width);
planner.backdrop(blur(std_dev), clip, bounds);
planner.push_layer(bounds);
planner.pop_layer();
}
planner.finish()
}
#[test]
fn a_frame_with_no_glass_costs_one_pass() {
let plan = BackdropPlanner::new().finish();
assert!(plan.is_empty());
assert_eq!(plan.render_passes(), 1);
assert_eq!(plan.blur_passes(), 0);
}
#[test]
fn six_separated_panels_share_one_snapshot() {
let plan = plan_panels(6, 200.0, 100.0, 12.0);
assert_eq!(plan.batches.len(), 1, "{plan:?}");
assert_eq!(plan.batches[0].ops.len(), 6);
assert_eq!(plan.render_passes(), 2);
assert_eq!(plan.blur_passes(), 6);
}
#[test]
fn overlapping_panels_cannot_share_a_snapshot() {
let plan = plan_panels(6, 50.0, 100.0, 12.0);
assert_eq!(plan.batches.len(), 6, "glass over glass costs a pass each");
assert_eq!(plan.render_passes(), 7);
}
#[test]
fn a_gap_smaller_than_the_blur_radius_still_cuts() {
let touching = plan_panels(2, 110.0, 100.0, 12.0);
assert_eq!(
touching.batches.len(),
2,
"a 10px gap is inside a 12px blur's reach"
);
let clear = plan_panels(2, 110.0, 100.0, 1.0);
assert_eq!(clear.batches.len(), 1);
}
#[test]
fn a_fill_between_two_panels_cuts_only_if_it_lands_in_the_blur_region() {
let elsewhere = {
let mut planner = BackdropPlanner::new();
let (clip, bounds) = panel(0.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.draw(Rect::new(1000.0, 0.0, 1100.0, 100.0));
let (clip, bounds) = panel(200.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.finish()
};
assert_eq!(elsewhere.batches.len(), 1, "a distant fill is irrelevant");
let underneath = {
let mut planner = BackdropPlanner::new();
let (clip, bounds) = panel(0.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.draw(Rect::new(200.0, 0.0, 300.0, 100.0));
let (clip, bounds) = panel(200.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.finish()
};
assert_eq!(
underneath.batches.len(),
2,
"the second panel's backdrop changed after the snapshot"
);
}
#[test]
fn draws_inside_a_layer_are_covered_by_the_layer() {
let mut planner = BackdropPlanner::new();
let (clip, bounds) = panel(0.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.push_layer(bounds);
planner.draw(Rect::new(200.0, 0.0, 300.0, 100.0));
planner.pop_layer();
let (clip, bounds) = panel(200.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
let plan = planner.finish();
assert_eq!(plan.batches.len(), 1, "{plan:?}");
}
#[test]
fn draws_shallower_than_the_batch_still_count() {
let mut planner = BackdropPlanner::new();
planner.push_layer(Rect::new(0.0, 0.0, 1000.0, 100.0));
let (clip, bounds) = panel(0.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
planner.pop_layer();
planner.draw(Rect::new(200.0, 0.0, 300.0, 100.0));
planner.push_layer(Rect::new(0.0, 0.0, 1000.0, 100.0));
let (clip, bounds) = panel(200.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
let plan = planner.finish();
assert_eq!(plan.batches.len(), 2, "{plan:?}");
}
#[test]
fn a_batch_gets_conservative_once_it_is_tracking_too_many_regions() {
let mut planner = BackdropPlanner::new();
let (clip, bounds) = panel(0.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
for index in 0..MAX_OCCUPIED_REGIONS + 4 {
let x = 1000.0 + index as f64 * 10.0;
planner.draw(Rect::new(x, 0.0, x + 5.0, 100.0));
}
let (clip, bounds) = panel(200.0, 100.0);
planner.backdrop(blur(4.0), clip, bounds);
let plan = planner.finish();
assert_eq!(
plan.batches.len(),
2,
"a collapsed occupancy list spans the gap, so the batch cuts"
);
}
#[test]
fn the_source_region_is_the_panel_grown_by_the_filter() {
let mut planner = BackdropPlanner::new();
let (clip, bounds) = panel(100.0, 100.0);
planner.backdrop(blur(10.0), clip, bounds);
let plan = planner.finish();
let op = &plan.batches[0].ops[0];
assert_eq!(op.bounds, bounds);
assert!(
op.source.x0 < bounds.x0 && op.source.x1 > bounds.x1,
"the read region must grow past the panel, got {:?}",
op.source
);
}
}