use egui::{Color32, CornerRadius, Painter, Rect, Shape, Stroke, StrokeKind};
use crate::Theme;
use crate::effects::glow_rect;
use crate::look::EffectsPolicy;
use crate::overlay::glass_tint;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChromeStyle {
pub radius: f32,
pub tint_alpha: u8,
pub glow_intensity: f32,
pub glow_layers: u32,
pub border_width: f32,
}
impl Default for ChromeStyle {
fn default() -> Self {
Self { radius: 8.0, tint_alpha: 30, glow_intensity: 0.5, glow_layers: 5, border_width: 1.0 }
}
}
impl ChromeStyle {
pub fn for_policy(mut self, policy: EffectsPolicy) -> Self {
match policy {
EffectsPolicy::Full => {}
EffectsPolicy::Reduced => self.glow_intensity = 0.0,
EffectsPolicy::None => {
self.glow_intensity = 0.0;
self.tint_alpha = 0;
}
}
self
}
}
fn is_light(c: Color32) -> bool {
0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32 > 140.0
}
pub fn fill_shape(rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) -> Shape {
let radius = CornerRadius::same(style.radius as u8);
let fill = if policy.allows_transparency() && style.tint_alpha > 0 {
glass_tint(theme.panel_bg, style.tint_alpha, is_light(theme.panel_bg))
} else {
theme.panel_bg
};
Shape::rect_filled(rect, radius, fill)
}
pub fn edge(painter: &Painter, rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) {
if policy.allows_transparency() && style.glow_intensity > 0.0 && style.glow_layers > 0 {
glow_rect(painter, rect, theme.glow, style.glow_intensity, style.glow_layers);
}
if style.border_width > 0.0 {
painter.rect_stroke(
rect,
CornerRadius::same(style.radius as u8),
Stroke::new(style.border_width, theme.panel_stroke),
StrokeKind::Inside,
);
}
}
pub fn card(ui: &egui::Ui, rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) {
let painter = ui.painter();
painter.add(fill_shape(rect, theme, policy, style));
edge(painter, rect, theme, policy, style);
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
fn rect() -> Rect {
Rect::from_min_max(pos2(10.0, 10.0), pos2(210.0, 110.0))
}
#[test]
fn fill_is_translucent_under_full_and_opaque_under_none() {
let th = Theme::deep_space();
let style = ChromeStyle::default();
let fill_color = |policy: EffectsPolicy| -> Color32 {
match fill_shape(rect(), &th, policy, style.for_policy(policy)) {
Shape::Rect(r) => r.fill,
_ => panic!("expected a rect fill shape"),
}
};
let full = fill_color(EffectsPolicy::Full);
let none = fill_color(EffectsPolicy::None);
assert!(full.a() < 255, "glass fill is translucent under Full: a={}", full.a());
assert_eq!(none, th.panel_bg, "None paints the raw panel colour (no tint)");
assert!(none.a() > full.a(), "opaque card is stronger than the glass tint");
}
#[test]
fn for_policy_strips_glow_then_tint() {
let s = ChromeStyle::default();
assert!(s.for_policy(EffectsPolicy::Full).glow_intensity > 0.0);
assert_eq!(s.for_policy(EffectsPolicy::Reduced).glow_intensity, 0.0);
assert!(s.for_policy(EffectsPolicy::Reduced).tint_alpha > 0, "Reduced keeps glass");
let none = s.for_policy(EffectsPolicy::None);
assert_eq!(none.glow_intensity, 0.0);
assert_eq!(none.tint_alpha, 0, "None strips the glass tint too");
}
#[test]
fn card_tessellates_a_non_empty_frame() {
let ctx = egui::Context::default();
let input = egui::RawInput {
screen_rect: Some(Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 300.0))),
..Default::default()
};
let out = ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
card(ui, rect(), &Theme::deep_space(), EffectsPolicy::Full, ChromeStyle::default());
});
});
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, "card paints a non-empty frame");
}
}