1use egui::{Color32, CornerRadius, LayerId, Order, Rect, Ui, UiBuilder};
12
13use crate::look::{EffectsPolicy, SurfaceSpec};
14
15#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum Mask {
19 Rect(Rect),
20 RoundedRect { rect: Rect, radius: u8 },
21}
22
23impl Mask {
24 pub fn rect(self) -> Rect {
25 match self {
26 Mask::Rect(r) | Mask::RoundedRect { rect: r, .. } => r,
27 }
28 }
29 pub fn radius(self) -> u8 {
30 match self {
31 Mask::Rect(_) => 0,
32 Mask::RoundedRect { radius, .. } => radius,
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug)]
40pub struct OverlaySpec {
41 pub mask: Mask,
42 pub order: Order,
43 pub surface: SurfaceSpec,
44}
45
46impl OverlaySpec {
47 pub fn new(rect: Rect) -> Self {
49 Self { mask: Mask::Rect(rect), order: Order::Foreground, surface: SurfaceSpec::Opaque }
50 }
51 pub fn rounded(rect: Rect, radius: u8) -> Self {
52 Self { mask: Mask::RoundedRect { rect, radius }, order: Order::Foreground, surface: SurfaceSpec::Opaque }
53 }
54 pub fn with_surface(mut self, s: SurfaceSpec) -> Self {
55 self.surface = s;
56 self
57 }
58 pub fn with_order(mut self, o: Order) -> Self {
59 self.order = o;
60 self
61 }
62}
63
64pub fn overlay<R>(
71 host: &mut Ui,
72 spec: OverlaySpec,
73 effects: EffectsPolicy,
74 id_salt: impl std::hash::Hash,
75 guest: impl FnOnce(&mut Ui) -> R,
76) -> R {
77 let rect = spec.mask.rect();
78 let radius = CornerRadius::same(spec.mask.radius());
79 let layer = LayerId::new(spec.order, host.id().with(("facett_overlay", &id_salt_str(&id_salt))));
80
81 let resolved = spec.surface.resolve(effects);
83 if let Some(tint) = resolved.tint_color() {
84 let painter = host.ctx().layer_painter(layer);
85 painter.rect_filled(rect, radius, tint);
86 }
87
88 let mut result = None;
90 host.scope_builder(UiBuilder::new().layer_id(layer).max_rect(rect), |ui| {
91 ui.set_clip_rect(rect);
92 if let SurfaceSpec::Opacity(o) = resolved {
94 ui.set_opacity(o);
95 }
96 result = Some(guest(ui));
97 });
98 result.expect("guest closure ran")
99}
100
101pub fn raise(ui: &Ui, order: Order, id_salt: impl std::hash::Hash) {
103 let layer = LayerId::new(order, ui.id().with(("facett_overlay", &id_salt_str(&id_salt))));
104 ui.ctx().move_to_top(layer);
105}
106
107fn id_salt_str(h: &impl std::hash::Hash) -> String {
108 use std::hash::Hasher as _;
109 let mut s = std::collections::hash_map::DefaultHasher::new();
110 h.hash(&mut s);
111 format!("{:x}", s.finish())
112}
113
114pub fn glass_tint(base: Color32, alpha: u8, on_light: bool) -> Color32 {
117 let a = if on_light { alpha.saturating_add(30) } else { alpha };
118 Color32::from_rgba_unmultiplied(base.r(), base.g(), base.b(), a)
119}
120
121#[cfg(test)]
122mod tests {
123 use egui::{Color32, pos2, vec2};
124
125 use super::*;
126 use crate::look::SurfaceSpec;
127
128 #[test]
129 fn glass_degrades_to_tint_off_wgpu_and_opaque_under_none() {
130 let frosted = SurfaceSpec::Frosted { blur_radius: 8.0, tint: [10, 12, 26, 180] };
131 assert_eq!(frosted.resolve(EffectsPolicy::Reduced), SurfaceSpec::Tint([10, 12, 26, 180]));
133 assert_eq!(frosted.resolve(EffectsPolicy::None), SurfaceSpec::Opaque);
135 }
136
137 #[test]
138 fn glass_tint_is_stronger_on_light() {
139 let base = Color32::from_rgb(10, 12, 26);
140 let dark = glass_tint(base, 160, false);
141 let light = glass_tint(base, 160, true);
142 assert!(light.a() > dark.a(), "light backgrounds need stronger glass edges (SURF-4)");
143 }
144
145 #[test]
146 #[allow(deprecated)]
147 fn overlay_guest_renders_into_the_masked_region() {
148 let ctx = egui::Context::default();
151 crate::look::Theme::windows_dark().apply(&ctx);
152 let mut guest_ran = false;
153 let input = egui::RawInput {
154 screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
155 ..Default::default()
156 };
157 let out = ctx.run(input, |ctx| {
158 egui::CentralPanel::default().show(ctx, |ui| {
159 ui.label("host map");
160 let sub = Rect::from_min_size(pos2(400.0, 100.0), vec2(300.0, 200.0));
161 let spec = OverlaySpec::rounded(sub, 8).with_surface(SurfaceSpec::Tint([10, 12, 26, 180]));
162 overlay(ui, spec, EffectsPolicy::Full, "df", |ui| {
163 ui.label("guest dataframe");
164 guest_ran = true;
165 });
166 });
167 });
168 assert!(guest_ran, "guest closure ran");
169 let prims = ctx.tessellate(out.shapes, out.pixels_per_point);
170 let verts: usize = prims
171 .iter()
172 .map(|p| match &p.primitive {
173 egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
174 _ => 0,
175 })
176 .sum();
177 assert!(verts > 0, "host + overlaid guest tessellate to a non-empty frame (picturable)");
178 }
179}