1use egui::{
5 Color32, FontSelection, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText,
6 WidgetType,
7};
8
9use crate::theme::{with_alpha, Theme};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum AvatarSize {
17 XSmall,
19 Small,
21 Medium,
23 Large,
25 XLarge,
27}
28
29impl AvatarSize {
30 fn diameter(self) -> f32 {
31 match self {
32 AvatarSize::XSmall => 20.0,
33 AvatarSize::Small => 28.0,
34 AvatarSize::Medium => 36.0,
35 AvatarSize::Large => 48.0,
36 AvatarSize::XLarge => 64.0,
37 }
38 }
39
40 fn font_size(self) -> f32 {
41 match self {
42 AvatarSize::XSmall => 10.0,
43 AvatarSize::Small => 11.5,
44 AvatarSize::Medium => 13.0,
45 AvatarSize::Large => 16.0,
46 AvatarSize::XLarge => 22.0,
47 }
48 }
49
50 fn dot_diameter(self) -> f32 {
51 match self {
52 AvatarSize::XSmall | AvatarSize::Small => 7.0,
53 AvatarSize::Medium => 10.0,
54 AvatarSize::Large => 12.0,
55 AvatarSize::XLarge => 14.0,
56 }
57 }
58
59 fn dot_border(self) -> f32 {
60 match self {
61 AvatarSize::XSmall | AvatarSize::Small => 1.5,
62 _ => 2.0,
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub enum AvatarTone {
74 Sky,
76 Green,
78 Amber,
80 Red,
82 Purple,
84 Neutral,
86}
87
88const AUTO_TONES: [AvatarTone; 5] = [
89 AvatarTone::Sky,
90 AvatarTone::Green,
91 AvatarTone::Amber,
92 AvatarTone::Red,
93 AvatarTone::Purple,
94];
95
96impl AvatarTone {
97 pub fn from_text(s: &str) -> Self {
99 let mut h: u32 = 0x811c_9dc5;
101 for b in s.bytes() {
102 h ^= b as u32;
103 h = h.wrapping_mul(0x0100_0193);
104 }
105 AUTO_TONES[(h as usize) % AUTO_TONES.len()]
106 }
107
108 fn colours(self, theme: &Theme) -> (Color32, Color32) {
109 let p = &theme.palette;
110 match self {
111 AvatarTone::Sky => (with_alpha(p.sky, 51), p.sky),
112 AvatarTone::Green => (with_alpha(p.green, 46), p.success),
113 AvatarTone::Amber => (with_alpha(p.warning, 51), p.warning),
114 AvatarTone::Red => (with_alpha(p.danger, 51), p.danger),
115 AvatarTone::Purple => (with_alpha(p.purple, 51), p.purple),
116 AvatarTone::Neutral => (with_alpha(p.text_muted, 40), p.text_muted),
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
123pub enum AvatarPresence {
124 Online,
126 Busy,
128 Away,
130 Offline,
132}
133
134impl AvatarPresence {
135 fn colour(self, theme: &Theme) -> Color32 {
136 let p = &theme.palette;
137 match self {
138 AvatarPresence::Online => p.success,
139 AvatarPresence::Busy => p.danger,
140 AvatarPresence::Away => p.warning,
141 AvatarPresence::Offline => p.text_faint,
142 }
143 }
144}
145
146#[must_use = "Add the avatar with `ui.add(...)`."]
160pub struct Avatar {
161 initials: WidgetText,
162 size: AvatarSize,
163 tone: Option<AvatarTone>,
164 presence: Option<AvatarPresence>,
165 surface: Option<Color32>,
166 ring: bool,
167}
168
169impl std::fmt::Debug for Avatar {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("Avatar")
172 .field("initials", &self.initials.text())
173 .field("size", &self.size)
174 .field("tone", &self.tone)
175 .field("presence", &self.presence)
176 .field("ring", &self.ring)
177 .finish()
178 }
179}
180
181impl Avatar {
182 pub fn new(initials: impl Into<WidgetText>) -> Self {
185 Self {
186 initials: initials.into(),
187 size: AvatarSize::Medium,
188 tone: None,
189 presence: None,
190 surface: None,
191 ring: false,
192 }
193 }
194
195 #[inline]
197 pub fn size(mut self, size: AvatarSize) -> Self {
198 self.size = size;
199 self
200 }
201
202 #[inline]
205 pub fn tone(mut self, tone: AvatarTone) -> Self {
206 self.tone = Some(tone);
207 self
208 }
209
210 #[inline]
212 pub fn presence(mut self, presence: AvatarPresence) -> Self {
213 self.presence = Some(presence);
214 self
215 }
216
217 #[inline]
224 pub fn surface(mut self, color: Color32) -> Self {
225 self.surface = Some(color);
226 self
227 }
228
229 #[inline]
233 pub fn ring(mut self, ring: bool) -> Self {
234 self.ring = ring;
235 self
236 }
237
238 fn paint(&self, ui: &mut Ui, rect: egui::Rect) {
239 if !ui.is_rect_visible(rect) {
240 return;
241 }
242 let theme = Theme::current(ui.ctx());
243 let p = &theme.palette;
244 let surface = self.surface.unwrap_or(p.bg);
245 let initials_text = self.initials.text();
246 let tone = self
247 .tone
248 .unwrap_or_else(|| AvatarTone::from_text(initials_text));
249 let (bg, fg) = tone.colours(&theme);
250
251 let painter = ui.painter();
252 let center = rect.center();
253 let r = self.size.diameter() * 0.5;
254
255 painter.circle_filled(center, r, bg);
256
257 if !initials_text.is_empty() {
258 let font_size = self.size.font_size();
259 let galley = WidgetText::from(
260 egui::RichText::new(initials_text)
261 .color(fg)
262 .size(font_size)
263 .strong(),
264 )
265 .into_galley(
266 ui,
267 Some(egui::TextWrapMode::Extend),
268 f32::INFINITY,
269 FontSelection::FontId(egui::FontId::proportional(font_size)),
270 );
271 let pos = center - galley.size() * 0.5;
272 ui.painter().galley(pos, galley, fg);
273 }
274
275 if self.ring {
276 ui.painter()
277 .circle_stroke(center, r + 1.0, Stroke::new(2.0, surface));
278 }
279
280 if let Some(presence) = self.presence {
281 let dot_d = self.size.dot_diameter();
282 let border_w = self.size.dot_border();
283 let off = r + 1.0 - dot_d * 0.5;
284 let dot_center = center + Vec2::splat(off);
285 let outer_r = dot_d * 0.5 + border_w;
286 ui.painter().circle_filled(dot_center, outer_r, surface);
287 ui.painter()
288 .circle_filled(dot_center, dot_d * 0.5, presence.colour(&theme));
289 }
290 }
291}
292
293impl Widget for Avatar {
294 fn ui(self, ui: &mut Ui) -> Response {
295 let diameter = self.size.diameter();
296 let (rect, response) = ui.allocate_exact_size(Vec2::splat(diameter), Sense::hover());
297 self.paint(ui, rect);
298
299 let label = self.initials.text();
300 let owned = if label.is_empty() {
301 "avatar".to_string()
302 } else {
303 label.to_string()
304 };
305 response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, true, &owned));
306 response
307 }
308}
309
310#[must_use = "Add the group with `ui.add(...)`."]
330pub struct AvatarGroup {
331 items: Vec<Avatar>,
332 overflow: Option<usize>,
333 overlap: f32,
334 surface: Option<Color32>,
335 size: AvatarSize,
336}
337
338impl std::fmt::Debug for AvatarGroup {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 f.debug_struct("AvatarGroup")
341 .field("items", &self.items.len())
342 .field("overflow", &self.overflow)
343 .field("overlap", &self.overlap)
344 .field("size", &self.size)
345 .finish()
346 }
347}
348
349impl Default for AvatarGroup {
350 fn default() -> Self {
351 Self::new()
352 }
353}
354
355impl AvatarGroup {
356 pub fn new() -> Self {
358 Self {
359 items: Vec::new(),
360 overflow: None,
361 overlap: 10.0,
362 surface: None,
363 size: AvatarSize::Medium,
364 }
365 }
366
367 #[inline]
370 pub fn item(mut self, avatar: Avatar) -> Self {
371 self.items.push(avatar);
372 self
373 }
374
375 #[inline]
378 pub fn overflow(mut self, n: usize) -> Self {
379 self.overflow = Some(n);
380 self
381 }
382
383 #[inline]
385 pub fn overlap(mut self, overlap: f32) -> Self {
386 self.overlap = overlap;
387 self
388 }
389
390 #[inline]
393 pub fn size(mut self, size: AvatarSize) -> Self {
394 self.size = size;
395 self
396 }
397
398 #[inline]
402 pub fn surface(mut self, color: Color32) -> Self {
403 self.surface = Some(color);
404 self
405 }
406}
407
408impl Widget for AvatarGroup {
409 fn ui(self, ui: &mut Ui) -> Response {
410 let theme = Theme::current(ui.ctx());
411 let surface = self.surface.unwrap_or(theme.palette.bg);
412 let diameter = self.size.diameter();
413 let count = self.items.len() + usize::from(self.overflow.is_some());
414 if count == 0 {
415 let (_, response) = ui.allocate_exact_size(Vec2::ZERO, Sense::hover());
416 return response;
417 }
418 let total_w = diameter * count as f32 - self.overlap * (count.saturating_sub(1)) as f32;
419 let (rect, response) = ui.allocate_exact_size(Vec2::new(total_w, diameter), Sense::hover());
420
421 let mut x = rect.left();
422 for avatar in self.items {
423 let avatar = avatar.size(self.size).surface(surface).ring(true);
424 let cell = egui::Rect::from_min_size(egui::pos2(x, rect.top()), Vec2::splat(diameter));
425 avatar.paint(ui, cell);
426 x += diameter - self.overlap;
427 }
428 if let Some(n) = self.overflow {
429 let label = format!("+{}", n);
430 let avatar = Avatar::new(label)
431 .size(self.size)
432 .tone(AvatarTone::Neutral)
433 .surface(surface)
434 .ring(true);
435 let cell = egui::Rect::from_min_size(egui::pos2(x, rect.top()), Vec2::splat(diameter));
436 avatar.paint(ui, cell);
437 }
438
439 response.widget_info(|| WidgetInfo::labeled(WidgetType::Other, true, "avatar group"));
440 response
441 }
442}