use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::{Canvas, Paint, PaintStyle, Rect, RRect};
use crate::engine::renderer::{asset_cache, paint_from_hex};
use crate::error::RustmotionError;
use crate::layout::{Constraints, LayoutNode};
use crate::schema::LayerStyle;
use crate::traits::{RenderContext, TimingConfig, Widget};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AvatarStatus {
Online,
Offline,
Away,
#[serde(rename = "none")]
NoStatus,
}
impl Default for AvatarStatus {
fn default() -> Self {
Self::NoStatus
}
}
impl AvatarStatus {
fn default_color(&self) -> &str {
match self {
AvatarStatus::Online => "#22C55E",
AvatarStatus::Offline => "#9CA3AF",
AvatarStatus::Away => "#F59E0B",
AvatarStatus::NoStatus => "",
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Avatar {
pub src: String,
#[serde(default = "default_avatar_size")]
pub size: f32,
#[serde(default)]
pub border_color: Option<String>,
#[serde(default)]
pub border_width: Option<f32>,
#[serde(default)]
pub status: AvatarStatus,
#[serde(default)]
pub status_color: Option<String>,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: LayerStyle,
}
fn default_avatar_size() -> f32 {
64.0
}
crate::impl_traits!(Avatar {
Animatable => style,
Timed => timing,
Styled => style,
});
impl Widget for Avatar {
fn render(
&self,
canvas: &Canvas,
layout: &LayoutNode,
_ctx: &RenderContext,
_props: &crate::engine::animator::AnimatedProperties,
) -> Result<()> {
let w = layout.width;
let h = layout.height;
let cache = asset_cache();
let img = if let Some(cached) = cache.get(&self.src) {
cached.clone()
} else {
let data = std::fs::read(&self.src)
.map_err(|e| RustmotionError::ImageLoad { path: self.src.clone(), reason: e.to_string() })?;
let skia_data = skia_safe::Data::new_copy(&data);
let decoded = skia_safe::Image::from_encoded(skia_data)
.ok_or_else(|| RustmotionError::ImageDecode { path: self.src.clone() })?;
cache.insert(self.src.clone(), decoded.clone());
decoded
};
let oval_rect = Rect::from_xywh(0.0, 0.0, w, h);
let oval_rrect = RRect::new_oval(oval_rect);
canvas.save();
canvas.clip_rrect(oval_rrect, skia_safe::ClipOp::Intersect, true);
let img_w = img.width() as f32;
let img_h = img.height() as f32;
let scale = (w / img_w).max(h / img_h);
let draw_w = img_w * scale;
let draw_h = img_h * scale;
let offset_x = (w - draw_w) / 2.0;
let offset_y = (h - draw_h) / 2.0;
let dst = Rect::from_xywh(offset_x, offset_y, draw_w, draw_h);
canvas.draw_image_rect(img, None, dst, &Paint::default());
canvas.restore();
let border_width = self.border_width.unwrap_or(0.0);
if border_width > 0.0 {
if let Some(border_color) = &self.border_color {
let mut border_paint = paint_from_hex(border_color);
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_stroke_width(border_width);
border_paint.set_anti_alias(true);
let inset = border_width / 2.0;
let border_rect = Rect::from_xywh(inset, inset, w - border_width, h - border_width);
canvas.draw_oval(border_rect, &border_paint);
}
}
if !matches!(self.status, AvatarStatus::NoStatus) {
let dot_radius = w * 0.15;
let dot_color = self
.status_color
.as_deref()
.unwrap_or_else(|| self.status.default_color());
let mut dot_paint = paint_from_hex(dot_color);
dot_paint.set_style(PaintStyle::Fill);
dot_paint.set_anti_alias(true);
let cx = w - dot_radius;
let cy = h - dot_radius;
canvas.draw_circle((cx, cy), dot_radius, &dot_paint);
let mut border_paint = paint_from_hex("#FFFFFF");
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_stroke_width(2.0);
border_paint.set_anti_alias(true);
canvas.draw_circle((cx, cy), dot_radius, &border_paint);
}
Ok(())
}
fn measure(&self, _constraints: &Constraints) -> (f32, f32) {
(self.size, self.size)
}
}