use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use backdrop_blur_core::{BlurError, BlurRequest, GlRegion, RepaintPolicy, Scale};
use backdrop_blur_glow::{FramebufferSize, GlowBlur, current_draw_framebuffer};
use crate::Surface;
pub struct GrabPassRenderer {
blur: Arc<Mutex<GlowBlur>>,
ran: Arc<AtomicBool>,
warned: Arc<AtomicBool>,
}
impl GrabPassRenderer {
pub fn new(gl: &Arc<glow::Context>) -> Result<Self, BlurError> {
let blur = GlowBlur::new(gl)?;
Ok(Self {
blur: Arc::new(Mutex::new(blur)),
ran: Arc::new(AtomicBool::new(false)),
warned: Arc::new(AtomicBool::new(false)),
})
}
pub fn destroy(&self, gl: &glow::Context) {
let mut guard = match self.blur.lock() {
Ok(guard) => guard,
Err(poisoned) => {
self.blur.clear_poison();
poisoned.into_inner()
}
};
guard.destroy(gl);
}
pub fn took_effect(&self) -> bool {
self.ran.swap(false, Ordering::Relaxed)
}
pub fn frost(&self, ui: &egui::Ui, surface: Surface) {
match surface.repaint {
RepaintPolicy::Live => ui.ctx().request_repaint(),
RepaintPolicy::Bounded(after) => ui.ctx().request_repaint_after(after),
RepaintPolicy::Static => {}
}
let blur = Arc::clone(&self.blur);
let ran = Arc::clone(&self.ran);
let warned = Arc::clone(&self.warned);
let callback = egui_glow::CallbackFn::new(move |info, painter| {
ran.store(true, Ordering::Relaxed);
let gl = painter.gl();
let Some((region, framebuffer_size)) = callback_region(&info) else {
return; };
let mut guard = match blur.lock() {
Ok(guard) => guard,
Err(poisoned) => {
log::warn!(
"backdrop-blur grab-pass mutex was poisoned by a prior panic; recovered"
);
blur.clear_poison();
poisoned.into_inner()
}
};
let request = BlurRequest {
source_region: region.into_region(),
target_rect: region.into_region(),
strength: surface.strength,
tint: surface.tint,
corner_radius: surface.corner_radius,
opacity: surface.opacity,
};
let target = current_draw_framebuffer(gl);
match guard.frost_region(gl, target, region, framebuffer_size, &request) {
Ok(()) => warned.store(false, Ordering::Relaxed),
Err(e) => {
if !warned.swap(true, Ordering::Relaxed) {
log::warn!("backdrop-blur grab-pass frost failed: {e}");
}
}
}
});
ui.painter().add(egui::PaintCallback {
rect: surface.rect,
callback: Arc::new(callback),
});
}
}
fn callback_region(info: &egui::PaintCallbackInfo) -> Option<(GlRegion, FramebufferSize)> {
let ppp = info.pixels_per_point;
let to_gl = |v: &egui::epaint::ViewportInPixels| {
GlRegion::from_bottom_px(
[v.left_px.max(0) as u32, v.from_bottom_px.max(0) as u32],
[v.width_px.max(0) as u32, v.height_px.max(0) as u32],
Scale::new(ppp),
)
};
let viewport = to_gl(&info.viewport_in_pixels());
let clip = to_gl(&info.clip_rect_in_pixels());
let region = viewport.intersect(&clip)?.clip_to(info.screen_size_px)?;
Some((region, FramebufferSize(info.screen_size_px)))
}
#[cfg(test)]
mod tests {
use super::*;
fn info(
viewport: egui::Rect,
clip: egui::Rect,
screen: [u32; 2],
ppp: f32,
) -> egui::PaintCallbackInfo {
egui::PaintCallbackInfo {
viewport,
clip_rect: clip,
pixels_per_point: ppp,
screen_size_px: screen,
}
}
fn rect(min: (f32, f32), size: (f32, f32)) -> egui::Rect {
egui::Rect::from_min_size(egui::pos2(min.0, min.1), egui::vec2(size.0, size.1))
}
#[test]
fn callback_region_maps_egui_top_to_gl_high_y_no_flip() {
let panel = rect((0.0, 0.0), (100.0, 50.0));
let (region, fb) =
callback_region(&info(panel, panel, [800, 600], 1.0)).expect("non-empty");
assert_eq!(region.origin_bl(), [0, 550]);
assert_eq!(region.size(), [100, 50]);
assert_eq!(fb, FramebufferSize([800, 600]));
}
#[test]
fn callback_region_maps_egui_bottom_to_gl_low_y() {
let panel = rect((0.0, 550.0), (100.0, 50.0));
let (region, _) = callback_region(&info(panel, panel, [800, 600], 1.0)).expect("non-empty");
assert_eq!(region.origin_bl(), [0, 0]);
assert_eq!(region.size(), [100, 50]);
}
#[test]
fn callback_region_scales_by_pixels_per_point() {
let panel = rect((10.0, 10.0), (100.0, 50.0));
let (region, fb) =
callback_region(&info(panel, panel, [1600, 1200], 2.0)).expect("non-empty");
assert_eq!(region.size(), [200, 100]);
assert_eq!(region.origin_bl(), [20, 1200 - 100 - 20]);
assert_eq!(fb, FramebufferSize([1600, 1200]));
}
#[test]
fn callback_region_is_none_when_clip_is_disjoint() {
let viewport = rect((0.0, 0.0), (100.0, 100.0));
let clip = rect((400.0, 400.0), (50.0, 50.0));
assert!(callback_region(&info(viewport, clip, [800, 600], 1.0)).is_none());
}
}