1use crate::theme::*;
4use crate::visuals::FrameVisuals;
5use crate::widgets::{ComboBox, Label};
6use egui::{Color32, Frame, Response, RichText, Sense, Stroke, Ui};
7
8pub const TINT_1: Color32 = Color32::from_rgba_premultiplied(216, 216, 216, 255);
10
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[derive(Debug, Clone, Copy)]
14pub struct Hsla {
15 pub h: f32,
17 pub s: f32,
19 pub l: f32,
21 pub a: f32,
23}
24
25impl Hsla {
26 pub fn from_color32(c: Color32) -> Self {
28 let (r, g, b) = unpremultiply_srgb(c);
29 let (h, s, l) = srgb_to_hsl(r, g, b);
30 Hsla {
31 h,
32 s: s * 100.0,
33 l: l * 100.0,
34 a: c.a() as f32 / 255.0,
35 }
36 }
37
38 pub fn from_hex(hex: &str) -> Option<Self> {
40 match Color32::from_hex(hex) {
41 Ok(c) => Some(Self::from_color32(c)),
42 Err(_) => None,
43 }
44 }
45
46 pub fn to_color32(&self) -> Color32 {
48 let (r, g, b, a) = self.to_rgba_components();
49 Color32::from_rgba_unmultiplied(r, g, b, a)
50 }
51
52 pub fn to_rgba_components(&self) -> (u8, u8, u8, u8) {
54 let (r, g, b) = hsl_to_srgb(self.h, self.s / 100.0, self.l / 100.0);
55 (
56 channel_to_u8(r),
57 channel_to_u8(g),
58 channel_to_u8(b),
59 channel_to_u8(self.a),
60 )
61 }
62
63 pub fn shades(&self, num_shades: usize, direction: ShadeDirection) -> Vec<Color32> {
65 let mut shades = Vec::new();
66 let step = if direction == ShadeDirection::Lighter {
67 5.0
68 } else {
69 -5.0
70 };
71 let mut current = *self;
72 for _ in 0..num_shades {
73 shades.push(current.to_color32());
74 current.l = (current.l as f32 + step).clamp(0.0, 100.0);
75 }
76 shades
77 }
78}
79
80#[derive(Debug, PartialEq, Eq, Clone, Copy)]
82pub enum ShadeDirection {
83 Lighter,
85 Darker,
87}
88
89fn unpremultiply_srgb(c: Color32) -> (f32, f32, f32) {
91 let a = c.a();
92 if a == 0 {
93 (0.0, 0.0, 0.0)
94 } else if a == 255 {
95 (
96 c.r() as f32 / 255.0,
97 c.g() as f32 / 255.0,
98 c.b() as f32 / 255.0,
99 )
100 } else {
101 let a = a as f32;
102 (
103 c.r() as f32 / a,
104 c.g() as f32 / a,
105 c.b() as f32 / a,
106 )
107 }
108}
109
110fn srgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
112 let max = r.max(g).max(b);
113 let min = r.min(g).min(b);
114 let l = (max + min) * 0.5;
115 let d = max - min;
116
117 if d == 0.0 {
118 return (0.0, 0.0, l);
119 }
120
121 let s = d / (1.0 - (2.0 * l - 1.0).abs());
122 let h = if max == r {
123 (g - b) / d
124 } else if max == g {
125 (b - r) / d + 2.0
126 } else {
127 (r - g) / d + 4.0
128 };
129 (h.rem_euclid(6.0) * 60.0, s, l)
130}
131
132fn hsl_to_srgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
134 let s = s.clamp(0.0, 1.0);
135 let l = l.clamp(0.0, 1.0);
136 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
137 let hp = h.rem_euclid(360.0) / 60.0;
138 let x = c * (1.0 - (hp.rem_euclid(2.0) - 1.0).abs());
139 let m = l - c * 0.5;
140 let (r, g, b) = match hp as u8 {
141 0 => (c, x, 0.0),
142 1 => (x, c, 0.0),
143 2 => (0.0, c, x),
144 3 => (0.0, x, c),
145 4 => (x, 0.0, c),
146 _ => (c, 0.0, x),
147 };
148 (r + m, g + m, b + m)
149}
150
151fn channel_to_u8(channel: f32) -> u8 {
152 (channel.clamp(0.0, 1.0) * 255.0) as u8
153}
154
155pub fn frame_palette_changed(old: &ThemeColors, new: &ThemeColors) -> bool {
157 old.title_bar != new.title_bar
158 || old.bg != new.bg
159 || old.widget_bg != new.widget_bg
160 || old.hover != new.hover
161 || old.highlight != new.highlight
162 || old.border != new.border
163}
164
165pub fn remap_if_eq(slot: &mut Color32, old: Color32, new: Color32) {
167 if *slot == old {
168 *slot = new;
169 }
170}
171
172pub fn remap_frame(
175 frame: &mut Frame,
176 old_fill: Color32,
177 new_fill: Color32,
178 old_border: Color32,
179 new_border: Color32,
180) {
181 remap_if_eq(&mut frame.fill, old_fill, new_fill);
182 remap_if_eq(&mut frame.stroke.color, old_border, new_border);
183 remap_if_eq(&mut frame.shadow.color, old_border, new_border);
184}
185
186pub fn remap_frame_visuals(
189 visuals: &mut FrameVisuals,
190 old_hover: Color32,
191 new_hover: Color32,
192 old_click: Color32,
193 new_click: Color32,
194 old_highlight: Color32,
195 new_highlight: Color32,
196) {
197 remap_if_eq(&mut visuals.bg_on_hover, old_hover, new_hover);
198 remap_if_eq(&mut visuals.bg_on_click, old_click, new_click);
199 remap_if_eq(
200 &mut visuals.border_on_hover.1,
201 old_highlight,
202 new_highlight,
203 );
204 remap_if_eq(
205 &mut visuals.border_on_click.1,
206 old_highlight,
207 new_highlight,
208 );
209}
210
211pub fn theme_switcher(current_theme: &Theme, ui: &mut Ui) -> Option<Theme> {
215 let mut new_theme_opt = None;
216
217 let text = RichText::new(current_theme.kind.to_str()).size(current_theme.typography.normal);
218 let current = Label::new(text, None);
219
220 ComboBox::new("Theme switcher", current).width(200.0).show_ui(ui, |ui| {
221 ui.spacing_mut().item_spacing.y = 12.0;
222
223 for kind in ThemeKind::to_vec() {
224 let label = RichText::new(kind.to_str()).size(current_theme.typography.normal);
225 let label = Label::new(label, None)
226 .interactive(true)
227 .fill_width(true)
228 .sense(Sense::click())
229 .expand(Some(4.0));
230
231 if ui.add(label).clicked() {
232 let mut new_theme = Theme::new(kind);
233 new_theme_opt = Some(new_theme.clone());
234 new_theme.install(ui.ctx());
235 }
236 }
237 });
238 new_theme_opt
239}
240
241pub fn frame(
245 frame: &mut Frame,
246 visuals: FrameVisuals,
247 ui: &mut Ui,
248 add_contents: impl FnOnce(&mut Ui),
249) -> Response {
250 let mut frame = frame.begin(ui);
251 let res = frame.content_ui.scope(|ui| add_contents(ui));
252
253 if res.response.interact(Sense::click()).clicked() {
254 frame.frame = frame.frame.fill(visuals.bg_on_click);
255 frame.frame = frame.frame.stroke(Stroke::new(
256 visuals.border_on_click.0,
257 visuals.border_on_click.1,
258 ));
259 } else if res.response.hovered() {
260 frame.frame = frame.frame.fill(visuals.bg_on_hover);
261 frame.frame = frame.frame.stroke(Stroke::new(
262 visuals.border_on_hover.0,
263 visuals.border_on_hover.1,
264 ));
265 }
266
267 frame.end(ui);
268 res.response
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn assert_near_color(a: Color32, b: Color32) {
276 let dr = (a.r() as i16 - b.r() as i16).abs();
277 let dg = (a.g() as i16 - b.g() as i16).abs();
278 let db = (a.b() as i16 - b.b() as i16).abs();
279 assert!(
280 dr <= 1 && dg <= 1 && db <= 1 && a.a() == b.a(),
281 "roundtrip {a:?} -> {b:?}"
282 );
283 }
284
285 #[test]
286 fn color32_hsla_roundtrip() {
287 let samples = [
288 Color32::from_rgba_unmultiplied(0, 0, 0, 255),
289 Color32::from_rgba_unmultiplied(255, 255, 255, 255),
290 Color32::from_rgba_unmultiplied(255, 0, 0, 255),
291 Color32::from_rgba_unmultiplied(0, 255, 0, 255),
292 Color32::from_rgba_unmultiplied(0, 0, 255, 255),
293 Color32::from_rgba_unmultiplied(128, 64, 192, 128),
294 Color32::from_rgba_unmultiplied(26, 27, 38, 255),
295 Color32::from_rgba_unmultiplied(122, 162, 247, 200),
296 ];
297 for c in samples {
298 assert_near_color(c, Hsla::from_color32(c).to_color32());
299 }
300 }
301
302 #[test]
303 fn from_hex_parses_and_preserves_alpha() {
304 let hsla = Hsla::from_hex("#7aa2f7").unwrap();
305 let (r, g, b, a) = hsla.to_rgba_components();
306 assert_eq!((r, g, b, a), (122, 162, 247, 255));
307
308 let hsla = Hsla::from_hex("#7aa2f780").unwrap();
309 assert!((hsla.a - 128.0 / 255.0).abs() < 1e-5);
310 assert_eq!(hsla.to_color32().a(), 128);
311 }
312
313 #[test]
314 fn hsl_pure_red() {
315 let c = Hsla {
316 h: 0.0,
317 s: 100.0,
318 l: 50.0,
319 a: 1.0,
320 }
321 .to_color32();
322 assert_eq!(c, Color32::from_rgb(255, 0, 0));
323 }
324}