1use crate::theme::*;
4use crate::visuals::FrameVisuals;
5use crate::widgets::{ComboBox, Label};
6use egui::{Color32, Frame, Response, RichText, Sense, 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_hover, old_hover, new_hover);
198 remap_if_eq(&mut visuals.bg_click, old_click, new_click);
199 remap_if_eq(&mut visuals.bg_selected, old_highlight, new_highlight);
200 remap_if_eq(
201 &mut visuals.border_hover.color,
202 old_highlight,
203 new_highlight,
204 );
205 remap_if_eq(
206 &mut visuals.border_click.color,
207 old_highlight,
208 new_highlight,
209 );
210}
211
212pub fn theme_switcher(current_theme: &Theme, ui: &mut Ui) -> Option<Theme> {
216 let mut new_theme_opt = None;
217
218 let text = RichText::new(current_theme.kind.to_str()).size(current_theme.typography.normal);
219 let current = Label::new(text, None);
220
221 ComboBox::new("Theme switcher", current).width(200.0).show_ui(ui, |ui| {
222 ui.spacing_mut().item_spacing.y = 12.0;
223
224 for kind in ThemeKind::to_vec() {
225 let label = RichText::new(kind.to_str()).size(current_theme.typography.normal);
226 let label = Label::new(label, None)
227 .interactive(true)
228 .fill_width(true)
229 .sense(Sense::click())
230 .expand(Some(4.0));
231
232 if ui.add(label).clicked() {
233 let mut new_theme = Theme::new(kind);
234 new_theme_opt = Some(new_theme.clone());
235 new_theme.install(ui.ctx());
236 }
237 }
238 });
239 new_theme_opt
240}
241
242pub fn frame(
246 frame: &mut Frame,
247 visuals: FrameVisuals,
248 ui: &mut Ui,
249 add_contents: impl FnOnce(&mut Ui),
250) -> Response {
251 let mut frame = frame.begin(ui);
252 let res = frame.content_ui.scope(|ui| add_contents(ui));
253
254 if res.response.interact(Sense::click()).clicked() {
255 frame.frame = frame.frame.fill(visuals.bg_click);
256 frame.frame = frame.frame.stroke(visuals.border_click);
257 } else if res.response.hovered() {
258 frame.frame = frame.frame.fill(visuals.bg_hover);
259 frame.frame = frame.frame.stroke(visuals.border_hover);
260 }
261
262 frame.end(ui);
263 res.response
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn assert_near_color(a: Color32, b: Color32) {
271 let dr = (a.r() as i16 - b.r() as i16).abs();
272 let dg = (a.g() as i16 - b.g() as i16).abs();
273 let db = (a.b() as i16 - b.b() as i16).abs();
274 assert!(
275 dr <= 1 && dg <= 1 && db <= 1 && a.a() == b.a(),
276 "roundtrip {a:?} -> {b:?}"
277 );
278 }
279
280 #[test]
281 fn color32_hsla_roundtrip() {
282 let samples = [
283 Color32::from_rgba_unmultiplied(0, 0, 0, 255),
284 Color32::from_rgba_unmultiplied(255, 255, 255, 255),
285 Color32::from_rgba_unmultiplied(255, 0, 0, 255),
286 Color32::from_rgba_unmultiplied(0, 255, 0, 255),
287 Color32::from_rgba_unmultiplied(0, 0, 255, 255),
288 Color32::from_rgba_unmultiplied(128, 64, 192, 128),
289 Color32::from_rgba_unmultiplied(26, 27, 38, 255),
290 Color32::from_rgba_unmultiplied(122, 162, 247, 200),
291 ];
292 for c in samples {
293 assert_near_color(c, Hsla::from_color32(c).to_color32());
294 }
295 }
296
297 #[test]
298 fn from_hex_parses_and_preserves_alpha() {
299 let hsla = Hsla::from_hex("#7aa2f7").unwrap();
300 let (r, g, b, a) = hsla.to_rgba_components();
301 assert_eq!((r, g, b, a), (122, 162, 247, 255));
302
303 let hsla = Hsla::from_hex("#7aa2f780").unwrap();
304 assert!((hsla.a - 128.0 / 255.0).abs() < 1e-5);
305 assert_eq!(hsla.to_color32().a(), 128);
306 }
307
308 #[test]
309 fn hsl_pure_red() {
310 let c = Hsla {
311 h: 0.0,
312 s: 100.0,
313 l: 50.0,
314 a: 1.0,
315 }
316 .to_color32();
317 assert_eq!(c, Color32::from_rgb(255, 0, 0));
318 }
319}