use crate::ecs::{Entity, World};
use crate::render::command::{CompositeMode, DrawCommand};
use crate::render::renderer::Renderer;
use crate::render::sw::blur::{alpha_for_radius, iir_blur_inplace};
use crate::types::{Fixed, Point, Rect, Transform};
use crate::ui::view::{View, ViewCtx};
pub struct BackgroundBlur {
pub radius: Fixed,
pub spread: Fixed,
}
impl BackgroundBlur {
pub fn new(radius: impl Into<Fixed>) -> Self {
Self {
radius: radius.into(),
spread: Fixed::ZERO,
}
}
pub fn with_spread(mut self, spread: impl Into<Fixed>) -> Self {
self.spread = spread.into();
self
}
}
fn background_blur_render(
renderer: &mut dyn Renderer,
world: &World,
entity: Entity,
rect: &Rect,
ctx: &mut ViewCtx,
) {
let Some(bg) = world.get::<BackgroundBlur>(entity) else {
return;
};
if bg.radius <= Fixed::ZERO {
return;
}
let base_rect = ctx.transform.apply_rect_bbox(*rect);
let alpha = alpha_for_radius(bg.radius);
let padding = bg.radius + bg.spread;
let sample_rect = if padding > Fixed::ZERO {
base_rect.inflate(padding)
} else {
base_rect
};
if let Err(error) = renderer.prepare_readback(&sample_rect) {
ctx.record(Err(error));
return;
}
let mut tmp = match renderer.sample_target_region(&sample_rect) {
Ok(Some(texture)) => texture,
Ok(None) => return,
Err(error) => {
ctx.record(Err(error));
return;
}
};
{
let r = crate::types::Rect::new(
crate::types::Fixed::ZERO,
crate::types::Fixed::ZERO,
crate::types::Fixed::from_int(tmp.width as i32),
crate::types::Fixed::from_int(tmp.height as i32),
);
iir_blur_inplace(&mut tmp, alpha, r);
}
ctx.draw(
renderer,
&DrawCommand::Blit {
pos: Point::new(sample_rect.x, sample_rect.y),
size: Point::new(sample_rect.w, sample_rect.h),
transform: Transform::IDENTITY,
quad: None,
texture: &tmp,
opa: 255,
radius: Fixed::ZERO,
composite: CompositeMode::SourceOver,
},
ctx.clip,
);
}
pub fn view() -> View {
View::new("BackgroundBlur", 60, background_blur_render).with_filter::<BackgroundBlur>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::renderer::{DrawRequest, RenderError};
use crate::ui::Style;
use crate::ui::theme::WidgetState;
struct FailingReadback;
impl Renderer for FailingReadback {
fn submit(&mut self, _: &DrawRequest<'_, '_>) -> Result<(), RenderError> {
panic!("readback preparation failure must stop the draw")
}
fn flush(&mut self) {}
fn prepare_readback(&mut self, _: &Rect) -> Result<(), RenderError> {
Err(RenderError::BackendFailure)
}
fn sample_target_region(
&self,
_: &Rect,
) -> Result<Option<crate::render::texture::Texture<'static>>, RenderError> {
panic!("failed preparation must stop before target sampling")
}
}
#[test]
fn readback_preparation_failure_stops_before_sampling() {
let mut world = World::new();
let entity = world.spawn_empty();
world.insert(entity, BackgroundBlur::new(4));
let style = Style::default();
let clip = Rect::new(0, 0, 32, 32);
let mut ctx = ViewCtx {
style: &style,
transform: Transform::IDENTITY,
quad: None,
clip: &clip,
bg_handled: false,
state: WidgetState::Enabled,
error: None,
};
background_blur_render(
&mut FailingReadback,
&world,
entity,
&Rect::new(4, 4, 12, 12),
&mut ctx,
);
assert_eq!(ctx.error, Some(RenderError::BackendFailure));
}
}