use crate::theme::*;
use crate::visuals::FrameVisuals;
use crate::widgets::{ComboBox, Label};
use egui::{Color32, Frame, Response, RichText, Sense, Ui};
pub const TINT_1: Color32 = Color32::from_rgba_premultiplied(216, 216, 216, 255);
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy)]
pub struct Hsla {
pub h: f32,
pub s: f32,
pub l: f32,
pub a: f32,
}
impl Hsla {
pub fn from_color32(c: Color32) -> Self {
let (r, g, b) = unpremultiply_srgb(c);
let (h, s, l) = srgb_to_hsl(r, g, b);
Hsla {
h,
s: s * 100.0,
l: l * 100.0,
a: c.a() as f32 / 255.0,
}
}
pub fn from_hex(hex: &str) -> Option<Self> {
match Color32::from_hex(hex) {
Ok(c) => Some(Self::from_color32(c)),
Err(_) => None,
}
}
pub fn to_color32(&self) -> Color32 {
let (r, g, b, a) = self.to_rgba_components();
Color32::from_rgba_unmultiplied(r, g, b, a)
}
pub fn to_rgba_components(&self) -> (u8, u8, u8, u8) {
let (r, g, b) = hsl_to_srgb(self.h, self.s / 100.0, self.l / 100.0);
(
channel_to_u8(r),
channel_to_u8(g),
channel_to_u8(b),
channel_to_u8(self.a),
)
}
pub fn shades(&self, num_shades: usize, direction: ShadeDirection) -> Vec<Color32> {
let mut shades = Vec::new();
let step = if direction == ShadeDirection::Lighter {
5.0
} else {
-5.0
};
let mut current = *self;
for _ in 0..num_shades {
shades.push(current.to_color32());
current.l = (current.l as f32 + step).clamp(0.0, 100.0);
}
shades
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ShadeDirection {
Lighter,
Darker,
}
fn unpremultiply_srgb(c: Color32) -> (f32, f32, f32) {
let a = c.a();
if a == 0 {
(0.0, 0.0, 0.0)
} else if a == 255 {
(
c.r() as f32 / 255.0,
c.g() as f32 / 255.0,
c.b() as f32 / 255.0,
)
} else {
let a = a as f32;
(
c.r() as f32 / a,
c.g() as f32 / a,
c.b() as f32 / a,
)
}
}
fn srgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) * 0.5;
let d = max - min;
if d == 0.0 {
return (0.0, 0.0, l);
}
let s = d / (1.0 - (2.0 * l - 1.0).abs());
let h = if max == r {
(g - b) / d
} else if max == g {
(b - r) / d + 2.0
} else {
(r - g) / d + 4.0
};
(h.rem_euclid(6.0) * 60.0, s, l)
}
fn hsl_to_srgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
let s = s.clamp(0.0, 1.0);
let l = l.clamp(0.0, 1.0);
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let hp = h.rem_euclid(360.0) / 60.0;
let x = c * (1.0 - (hp.rem_euclid(2.0) - 1.0).abs());
let m = l - c * 0.5;
let (r, g, b) = match hp as u8 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(r + m, g + m, b + m)
}
fn channel_to_u8(channel: f32) -> u8 {
(channel.clamp(0.0, 1.0) * 255.0) as u8
}
pub fn frame_palette_changed(old: &ThemeColors, new: &ThemeColors) -> bool {
old.title_bar != new.title_bar
|| old.bg != new.bg
|| old.widget_bg != new.widget_bg
|| old.hover != new.hover
|| old.highlight != new.highlight
|| old.border != new.border
}
pub fn remap_if_eq(slot: &mut Color32, old: Color32, new: Color32) {
if *slot == old {
*slot = new;
}
}
pub fn remap_frame(
frame: &mut Frame,
old_fill: Color32,
new_fill: Color32,
old_border: Color32,
new_border: Color32,
) {
remap_if_eq(&mut frame.fill, old_fill, new_fill);
remap_if_eq(&mut frame.stroke.color, old_border, new_border);
remap_if_eq(&mut frame.shadow.color, old_border, new_border);
}
pub fn remap_frame_visuals(
visuals: &mut FrameVisuals,
old_hover: Color32,
new_hover: Color32,
old_click: Color32,
new_click: Color32,
old_highlight: Color32,
new_highlight: Color32,
) {
remap_if_eq(&mut visuals.bg_hover, old_hover, new_hover);
remap_if_eq(&mut visuals.bg_click, old_click, new_click);
remap_if_eq(&mut visuals.bg_selected, old_highlight, new_highlight);
remap_if_eq(
&mut visuals.border_hover.color,
old_highlight,
new_highlight,
);
remap_if_eq(
&mut visuals.border_click.color,
old_highlight,
new_highlight,
);
}
pub fn theme_switcher(current_theme: &Theme, ui: &mut Ui) -> Option<Theme> {
let mut new_theme_opt = None;
let text = RichText::new(current_theme.kind.to_str()).size(current_theme.typography.normal);
let current = Label::new(text, None);
ComboBox::new("Theme switcher", current).width(200.0).show_ui(ui, |ui| {
ui.spacing_mut().item_spacing.y = 12.0;
for kind in ThemeKind::to_vec() {
let label = RichText::new(kind.to_str()).size(current_theme.typography.normal);
let label = Label::new(label, None)
.interactive(true)
.fill_width(true)
.sense(Sense::click())
.expand(Some(4.0));
if ui.add(label).clicked() {
let mut new_theme = Theme::new(kind);
new_theme_opt = Some(new_theme.clone());
new_theme.install(ui.ctx());
}
}
});
new_theme_opt
}
pub fn frame(
frame: &mut Frame,
visuals: FrameVisuals,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let mut frame = frame.begin(ui);
let res = frame.content_ui.scope(|ui| add_contents(ui));
if res.response.interact(Sense::click()).clicked() {
frame.frame = frame.frame.fill(visuals.bg_click);
frame.frame = frame.frame.stroke(visuals.border_click);
} else if res.response.hovered() {
frame.frame = frame.frame.fill(visuals.bg_hover);
frame.frame = frame.frame.stroke(visuals.border_hover);
}
frame.end(ui);
res.response
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_near_color(a: Color32, b: Color32) {
let dr = (a.r() as i16 - b.r() as i16).abs();
let dg = (a.g() as i16 - b.g() as i16).abs();
let db = (a.b() as i16 - b.b() as i16).abs();
assert!(
dr <= 1 && dg <= 1 && db <= 1 && a.a() == b.a(),
"roundtrip {a:?} -> {b:?}"
);
}
#[test]
fn color32_hsla_roundtrip() {
let samples = [
Color32::from_rgba_unmultiplied(0, 0, 0, 255),
Color32::from_rgba_unmultiplied(255, 255, 255, 255),
Color32::from_rgba_unmultiplied(255, 0, 0, 255),
Color32::from_rgba_unmultiplied(0, 255, 0, 255),
Color32::from_rgba_unmultiplied(0, 0, 255, 255),
Color32::from_rgba_unmultiplied(128, 64, 192, 128),
Color32::from_rgba_unmultiplied(26, 27, 38, 255),
Color32::from_rgba_unmultiplied(122, 162, 247, 200),
];
for c in samples {
assert_near_color(c, Hsla::from_color32(c).to_color32());
}
}
#[test]
fn from_hex_parses_and_preserves_alpha() {
let hsla = Hsla::from_hex("#7aa2f7").unwrap();
let (r, g, b, a) = hsla.to_rgba_components();
assert_eq!((r, g, b, a), (122, 162, 247, 255));
let hsla = Hsla::from_hex("#7aa2f780").unwrap();
assert!((hsla.a - 128.0 / 255.0).abs() < 1e-5);
assert_eq!(hsla.to_color32().a(), 128);
}
#[test]
fn hsl_pure_red() {
let c = Hsla {
h: 0.0,
s: 100.0,
l: 50.0,
a: 1.0,
}
.to_color32();
assert_eq!(c, Color32::from_rgb(255, 0, 0));
}
}