use egui::{Color32, CornerRadius, LayerId, Order, Rect, Ui, UiBuilder};
use crate::look::{EffectsPolicy, SurfaceSpec};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Mask {
Rect(Rect),
RoundedRect { rect: Rect, radius: u8 },
}
impl Mask {
pub fn rect(self) -> Rect {
match self {
Mask::Rect(r) | Mask::RoundedRect { rect: r, .. } => r,
}
}
pub fn radius(self) -> u8 {
match self {
Mask::Rect(_) => 0,
Mask::RoundedRect { radius, .. } => radius,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct OverlaySpec {
pub mask: Mask,
pub order: Order,
pub surface: SurfaceSpec,
}
impl OverlaySpec {
pub fn new(rect: Rect) -> Self {
Self { mask: Mask::Rect(rect), order: Order::Foreground, surface: SurfaceSpec::Opaque }
}
pub fn rounded(rect: Rect, radius: u8) -> Self {
Self { mask: Mask::RoundedRect { rect, radius }, order: Order::Foreground, surface: SurfaceSpec::Opaque }
}
pub fn with_surface(mut self, s: SurfaceSpec) -> Self {
self.surface = s;
self
}
pub fn with_order(mut self, o: Order) -> Self {
self.order = o;
self
}
}
pub fn overlay<R>(
host: &mut Ui,
spec: OverlaySpec,
effects: EffectsPolicy,
id_salt: impl std::hash::Hash,
guest: impl FnOnce(&mut Ui) -> R,
) -> R {
let rect = spec.mask.rect();
let radius = CornerRadius::same(spec.mask.radius());
let layer = LayerId::new(spec.order, host.id().with(("facett_overlay", &id_salt_str(&id_salt))));
let resolved = spec.surface.resolve(effects);
if let Some(tint) = resolved.tint_color() {
let painter = host.ctx().layer_painter(layer);
painter.rect_filled(rect, radius, tint);
}
let mut result = None;
host.scope_builder(UiBuilder::new().layer_id(layer).max_rect(rect), |ui| {
ui.set_clip_rect(rect);
if let SurfaceSpec::Opacity(o) = resolved {
ui.set_opacity(o);
}
result = Some(guest(ui));
});
result.expect("guest closure ran")
}
pub fn raise(ui: &Ui, order: Order, id_salt: impl std::hash::Hash) {
let layer = LayerId::new(order, ui.id().with(("facett_overlay", &id_salt_str(&id_salt))));
ui.ctx().move_to_top(layer);
}
fn id_salt_str(h: &impl std::hash::Hash) -> String {
use std::hash::Hasher as _;
let mut s = std::collections::hash_map::DefaultHasher::new();
h.hash(&mut s);
format!("{:x}", s.finish())
}
pub fn glass_tint(base: Color32, alpha: u8, on_light: bool) -> Color32 {
let a = if on_light { alpha.saturating_add(30) } else { alpha };
Color32::from_rgba_unmultiplied(base.r(), base.g(), base.b(), a)
}
#[cfg(test)]
mod tests {
use egui::{Color32, pos2, vec2};
use super::*;
use crate::look::SurfaceSpec;
#[test]
fn glass_degrades_to_tint_off_wgpu_and_opaque_under_none() {
let frosted = SurfaceSpec::Frosted { blur_radius: 8.0, tint: [10, 12, 26, 180] };
assert_eq!(frosted.resolve(EffectsPolicy::Reduced), SurfaceSpec::Tint([10, 12, 26, 180]));
assert_eq!(frosted.resolve(EffectsPolicy::None), SurfaceSpec::Opaque);
}
#[test]
fn glass_tint_is_stronger_on_light() {
let base = Color32::from_rgb(10, 12, 26);
let dark = glass_tint(base, 160, false);
let light = glass_tint(base, 160, true);
assert!(light.a() > dark.a(), "light backgrounds need stronger glass edges (SURF-4)");
}
#[test]
#[allow(deprecated)]
fn overlay_guest_renders_into_the_masked_region() {
let ctx = egui::Context::default();
crate::look::Theme::windows_dark().apply(&ctx);
let mut guest_ran = false;
let input = egui::RawInput {
screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
..Default::default()
};
let out = ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("host map");
let sub = Rect::from_min_size(pos2(400.0, 100.0), vec2(300.0, 200.0));
let spec = OverlaySpec::rounded(sub, 8).with_surface(SurfaceSpec::Tint([10, 12, 26, 180]));
overlay(ui, spec, EffectsPolicy::Full, "df", |ui| {
ui.label("guest dataframe");
guest_ran = true;
});
});
});
assert!(guest_ran, "guest closure ran");
let prims = ctx.tessellate(out.shapes, out.pixels_per_point);
let verts: usize = prims
.iter()
.map(|p| match &p.primitive {
egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
_ => 0,
})
.sum();
assert!(verts > 0, "host + overlaid guest tessellate to a non-empty frame (picturable)");
}
}