1use egui::{vec2, Color32, FontId, Response, Sense, Stroke, Ui, Widget};
14use egui_components_theme::{Theme, ThemeColor};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
18pub enum AvatarShape {
19 #[default]
20 Circle,
21 Square,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum AvatarStatus {
28 Online,
29 Offline,
30 Busy,
31 Away,
32}
33
34pub struct Avatar {
35 initials: String,
36 size: f32,
37 shape: AvatarShape,
38 bg: Option<Color32>,
39 fg: Option<Color32>,
40 status: Option<AvatarStatus>,
41}
42
43impl Avatar {
44 pub fn new(initials: impl Into<String>) -> Self {
46 Self {
47 initials: initials.into(),
48 size: 40.0,
49 shape: AvatarShape::Circle,
50 bg: None,
51 fg: None,
52 status: None,
53 }
54 }
55
56 pub fn from_name(name: impl AsRef<str>) -> Self {
59 let name = name.as_ref();
60 let initials: String = name
61 .split_whitespace()
62 .filter_map(|w| w.chars().next())
63 .take(2)
64 .collect::<String>()
65 .to_uppercase();
66 let mut avatar = Self::new(if initials.is_empty() {
67 "?".to_string()
68 } else {
69 initials
70 });
71 avatar.bg = Some(color_seed(name));
72 avatar
73 }
74
75 pub fn size(mut self, px: f32) -> Self {
76 self.size = px;
77 self
78 }
79 pub fn small(self) -> Self {
80 self.size(28.0)
81 }
82 pub fn large(self) -> Self {
83 self.size(56.0)
84 }
85 pub fn shape(mut self, s: AvatarShape) -> Self {
86 self.shape = s;
87 self
88 }
89 pub fn square(self) -> Self {
90 self.shape(AvatarShape::Square)
91 }
92 pub fn background(mut self, c: Color32) -> Self {
94 self.bg = Some(c);
95 self
96 }
97 pub fn foreground(mut self, c: Color32) -> Self {
99 self.fg = Some(c);
100 self
101 }
102 pub fn status(mut self, s: AvatarStatus) -> Self {
103 self.status = Some(s);
104 self
105 }
106}
107
108impl Widget for Avatar {
109 fn ui(self, ui: &mut Ui) -> Response {
110 let theme = Theme::get(ui.ctx());
111 let c = theme.colors;
112
113 let (rect, response) = ui.allocate_exact_size(vec2(self.size, self.size), Sense::hover());
114
115 if ui.is_rect_visible(rect) {
116 let bg = self.bg.unwrap_or(c.secondary_background);
117 let fg = self.fg.unwrap_or_else(|| contrast_on(bg));
118 let painter = ui.painter();
119
120 match self.shape {
121 AvatarShape::Circle => {
122 painter.circle_filled(rect.center(), self.size * 0.5, bg);
123 }
124 AvatarShape::Square => {
125 painter.rect_filled(
126 rect,
127 egui::CornerRadius::same((self.size * 0.22) as u8),
128 bg,
129 );
130 }
131 }
132
133 let font = FontId::proportional(self.size * 0.4);
134 let galley = ui
135 .ctx()
136 .fonts_mut(|f| f.layout_no_wrap(self.initials.clone(), font, fg));
137 painter.galley_with_override_text_color(
138 rect.center() - galley.size() * 0.5,
139 galley,
140 fg,
141 );
142
143 if let Some(status) = self.status {
144 let dot_r = (self.size * 0.16).max(3.5);
145 let offset = self.size * 0.5 - dot_r * 0.7;
146 let center = rect.center() + vec2(offset, offset);
147 painter.circle_filled(center, dot_r + theme.metrics.border_width, c.background);
149 painter.circle(
150 center,
151 dot_r,
152 status_color(&c, status),
153 Stroke::NONE,
154 );
155 }
156 }
157
158 response
159 }
160}
161
162fn status_color(c: &ThemeColor, status: AvatarStatus) -> Color32 {
163 match status {
164 AvatarStatus::Online => c.success_background,
165 AvatarStatus::Offline => c.muted_foreground,
166 AvatarStatus::Busy => c.danger_background,
167 AvatarStatus::Away => c.warning_background,
168 }
169}
170
171fn contrast_on(bg: Color32) -> Color32 {
173 let luminance = 0.299 * bg.r() as f32 + 0.587 * bg.g() as f32 + 0.114 * bg.b() as f32;
174 if luminance > 140.0 {
175 Color32::from_rgb(0x0a, 0x0a, 0x0a)
176 } else {
177 Color32::from_rgb(0xfa, 0xfa, 0xfa)
178 }
179}
180
181fn color_seed(name: &str) -> Color32 {
184 let mut hash: u32 = 0x811c_9dc5;
186 for b in name.bytes() {
187 hash ^= b as u32;
188 hash = hash.wrapping_mul(0x0100_0193);
189 }
190 const PALETTE: [Color32; 6] = [
191 Color32::from_rgb(0x3b, 0x82, 0xf6), Color32::from_rgb(0x10, 0xb9, 0x81), Color32::from_rgb(0xf5, 0x9e, 0x0b), Color32::from_rgb(0xef, 0x44, 0x44), Color32::from_rgb(0x8b, 0x5c, 0xf6), Color32::from_rgb(0x06, 0xb6, 0xd4), ];
198 PALETTE[(hash as usize) % PALETTE.len()]
199}