1use egui::{Color32, CornerRadius, Painter, Rect, Shape, Stroke, StrokeKind};
19
20use crate::Theme;
21use crate::effects::glow_rect;
22use crate::look::EffectsPolicy;
23use crate::overlay::glass_tint;
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct ChromeStyle {
28 pub radius: f32,
30 pub tint_alpha: u8,
33 pub glow_intensity: f32,
35 pub glow_layers: u32,
37 pub border_width: f32,
39}
40
41impl Default for ChromeStyle {
42 fn default() -> Self {
44 Self { radius: 8.0, tint_alpha: 30, glow_intensity: 0.5, glow_layers: 5, border_width: 1.0 }
45 }
46}
47
48impl ChromeStyle {
49 pub fn for_policy(mut self, policy: EffectsPolicy) -> Self {
53 match policy {
54 EffectsPolicy::Full => {}
55 EffectsPolicy::Reduced => self.glow_intensity = 0.0,
56 EffectsPolicy::None => {
57 self.glow_intensity = 0.0;
58 self.tint_alpha = 0;
59 }
60 }
61 self
62 }
63}
64
65fn is_light(c: Color32) -> bool {
68 0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32 > 140.0
69}
70
71pub fn fill_shape(rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) -> Shape {
76 let radius = CornerRadius::same(style.radius as u8);
77 let fill = if policy.allows_transparency() && style.tint_alpha > 0 {
78 glass_tint(theme.panel_bg, style.tint_alpha, is_light(theme.panel_bg))
81 } else {
82 theme.panel_bg
84 };
85 Shape::rect_filled(rect, radius, fill)
86}
87
88pub fn edge(painter: &Painter, rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) {
91 if policy.allows_transparency() && style.glow_intensity > 0.0 && style.glow_layers > 0 {
93 glow_rect(painter, rect, theme.glow, style.glow_intensity, style.glow_layers);
94 }
95 if style.border_width > 0.0 {
97 painter.rect_stroke(
98 rect,
99 CornerRadius::same(style.radius as u8),
100 Stroke::new(style.border_width, theme.panel_stroke),
101 StrokeKind::Inside,
102 );
103 }
104}
105
106pub fn card(ui: &egui::Ui, rect: Rect, theme: &Theme, policy: EffectsPolicy, style: ChromeStyle) {
111 let painter = ui.painter();
112 painter.add(fill_shape(rect, theme, policy, style));
113 edge(painter, rect, theme, policy, style);
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use egui::pos2;
120
121 fn rect() -> Rect {
122 Rect::from_min_max(pos2(10.0, 10.0), pos2(210.0, 110.0))
123 }
124
125 #[test]
126 fn fill_is_translucent_under_full_and_opaque_under_none() {
127 let th = Theme::deep_space();
128 let style = ChromeStyle::default();
129 let fill_color = |policy: EffectsPolicy| -> Color32 {
130 match fill_shape(rect(), &th, policy, style.for_policy(policy)) {
131 Shape::Rect(r) => r.fill,
132 _ => panic!("expected a rect fill shape"),
133 }
134 };
135 let full = fill_color(EffectsPolicy::Full);
138 let none = fill_color(EffectsPolicy::None);
139 assert!(full.a() < 255, "glass fill is translucent under Full: a={}", full.a());
140 assert_eq!(none, th.panel_bg, "None paints the raw panel colour (no tint)");
141 assert!(none.a() > full.a(), "opaque card is stronger than the glass tint");
142 }
143
144 #[test]
145 fn for_policy_strips_glow_then_tint() {
146 let s = ChromeStyle::default();
147 assert!(s.for_policy(EffectsPolicy::Full).glow_intensity > 0.0);
148 assert_eq!(s.for_policy(EffectsPolicy::Reduced).glow_intensity, 0.0);
149 assert!(s.for_policy(EffectsPolicy::Reduced).tint_alpha > 0, "Reduced keeps glass");
150 let none = s.for_policy(EffectsPolicy::None);
151 assert_eq!(none.glow_intensity, 0.0);
152 assert_eq!(none.tint_alpha, 0, "None strips the glass tint too");
153 }
154
155 #[test]
156 fn card_tessellates_a_non_empty_frame() {
157 let ctx = egui::Context::default();
159 let input = egui::RawInput {
160 screen_rect: Some(Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 300.0))),
161 ..Default::default()
162 };
163 let out = ctx.run(input, |ctx| {
164 egui::CentralPanel::default().show(ctx, |ui| {
165 card(ui, rect(), &Theme::deep_space(), EffectsPolicy::Full, ChromeStyle::default());
166 });
167 });
168 let prims = ctx.tessellate(out.shapes, out.pixels_per_point);
169 let verts: usize = prims
170 .iter()
171 .map(|p| match &p.primitive {
172 egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
173 _ => 0,
174 })
175 .sum();
176 assert!(verts > 0, "card paints a non-empty frame");
177 }
178}