use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind};
use crate::geometry::{BorderRadius, Color, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AvatarSize {
XSmall,
Small,
#[default]
Medium,
Large,
XLarge,
Custom(u32),
}
impl AvatarSize {
pub fn pixels(&self) -> f32 {
match self {
AvatarSize::XSmall => 24.0,
AvatarSize::Small => 32.0,
AvatarSize::Medium => 40.0,
AvatarSize::Large => 64.0,
AvatarSize::XLarge => 96.0,
AvatarSize::Custom(size) => *size as f32,
}
}
pub fn font_size(&self) -> f32 {
self.pixels() * 0.4
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AvatarShape {
#[default]
Circle,
Rounded,
Square,
}
pub struct Avatar {
base: WidgetBase,
initials: Option<String>,
image_path: Option<String>,
fallback_initials: Option<String>,
size: AvatarSize,
shape: AvatarShape,
background_color: Option<Color>,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Avatar {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("avatar"),
initials: None,
image_path: None,
fallback_initials: None,
size: AvatarSize::default(),
shape: AvatarShape::default(),
background_color: None,
on_click: None,
}
}
pub fn initials(mut self, initials: impl Into<String>) -> Self {
let text = initials.into();
self.initials = Some(
text.chars()
.take(2)
.collect::<String>()
.to_uppercase()
);
self
}
pub fn image(mut self, path: impl Into<String>) -> Self {
self.image_path = Some(path.into());
self
}
pub fn fallback_initials(mut self, initials: impl Into<String>) -> Self {
let text = initials.into();
self.fallback_initials = Some(
text.chars()
.take(2)
.collect::<String>()
.to_uppercase()
);
self
}
pub fn size(mut self, size: AvatarSize) -> Self {
self.size = size;
self
}
pub fn shape(mut self, shape: AvatarShape) -> Self {
self.shape = shape;
self
}
pub fn background(mut self, color: Color) -> Self {
self.background_color = Some(color);
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
fn color_from_string(s: &str) -> Color {
let hash: u32 = s.bytes().fold(0u32, |acc, b| {
acc.wrapping_mul(31).wrapping_add(b as u32)
});
let hue = (hash % 360) as f32;
let saturation: f32 = 0.5;
let lightness: f32 = 0.6;
let c = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
let x = c * (1.0 - ((hue / 60.0) % 2.0 - 1.0).abs());
let m = lightness - c / 2.0;
let (r, g, b): (f32, f32, f32) = match (hue / 60.0) as u32 {
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),
};
Color::rgb(r + m, g + m, b + m)
}
fn border_radius(&self) -> BorderRadius {
let size = self.size.pixels();
match self.shape {
AvatarShape::Circle => BorderRadius::all(size / 2.0),
AvatarShape::Rounded => BorderRadius::all(size * 0.2),
AvatarShape::Square => BorderRadius::all(0.0),
}
}
}
impl Default for Avatar {
fn default() -> Self {
Self::new()
}
}
impl Widget for Avatar {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"avatar"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
let size = self.size.pixels();
Size::new(size, size)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let size = constraints.constrain(self.intrinsic_size(ctx));
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
let radius = self.border_radius();
let bg_color = self.background_color.unwrap_or_else(|| {
if let Some(ref initials) = self.initials {
Self::color_from_string(initials)
} else if let Some(ref fallback) = self.fallback_initials {
Self::color_from_string(fallback)
} else {
theme.colors.muted
}
});
painter.fill_rounded_rect(rect, bg_color, radius);
let text = self.initials.as_ref()
.or(self.fallback_initials.as_ref());
if let Some(initials) = text {
let font_size = self.size.font_size();
let text_width = initials.len() as f32 * font_size * 0.6;
let text_x = rect.x() + (rect.width() - text_width) / 2.0;
let text_y = rect.y() + (rect.height() + font_size * 0.8) / 2.0;
painter.draw_text(
initials,
Point::new(text_x, text_y),
Color::WHITE,
font_size,
);
}
if self.base.state.hovered && self.on_click.is_some() {
painter.stroke_rect(rect, theme.colors.ring.with_alpha(0.5), 2.0);
}
if self.base.state.focused {
let ring_rect = Rect::new(
rect.x() - 2.0,
rect.y() - 2.0,
rect.width() + 4.0,
rect.height() + 4.0,
);
painter.stroke_rect(ring_rect, theme.colors.ring, 2.0);
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if self.on_click.is_none() {
return EventResult::Ignored;
}
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Move | MouseEventKind::Enter => {
if in_bounds && !self.base.state.hovered {
self.base.state.hovered = true;
ctx.request_redraw();
} else if !in_bounds && self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Up if in_bounds && self.base.state.pressed => {
self.base.state.pressed = false;
if let Some(handler) = &self.on_click {
handler();
}
ctx.request_redraw();
return EventResult::Handled;
}
MouseEventKind::Down if in_bounds => {
self.base.state.pressed = true;
ctx.request_redraw();
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}