use std::{
collections::hash_map::DefaultHasher,
fmt,
hash::{Hash as _, Hasher as _},
sync::Arc,
};
use gpui_kit::{
AnyElement, Image, ImageFormat, ImageSource, IntoElement, Pixels, SharedString, Styled as _,
assets::IconName, component::Icon, img, px,
};
#[derive(Clone)]
pub enum ChatIcon {
Named(IconName),
Path(SharedString),
Data(Arc<[u8]>),
Png(Arc<Image>),
}
impl ChatIcon {
pub fn data(bytes: impl AsRef<[u8]>) -> Self {
Self::Data(Arc::from(bytes.as_ref()))
}
pub fn path(path: impl Into<SharedString>) -> Self {
Self::Path(path.into())
}
pub fn png(bytes: impl AsRef<[u8]>) -> Self {
let bytes = bytes.as_ref();
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
Self::Png(Arc::new(Image {
format: ImageFormat::Png,
bytes: bytes.to_vec(),
id: hasher.finish(),
}))
}
pub(crate) fn as_icon(&self) -> Option<Icon> {
match self {
Self::Named(name) => Some(Icon::new(*name)),
Self::Path(path) => Some(Icon::default().path(path.clone())),
Self::Data(bytes) => Some(Icon::default().data(bytes)),
Self::Png(_) => None,
}
}
pub(crate) fn element(&self, size: Option<Pixels>) -> AnyElement {
if let Self::Png(image) = self {
return img(ImageSource::Image(image.clone()))
.size(size.unwrap_or(px(24.)))
.into_any_element();
}
let icon = self
.as_icon()
.unwrap_or_else(|| Icon::new(IconName::Sparkles));
match size {
Some(size) => icon.size(size).into_any_element(),
None => icon.into_any_element(),
}
}
}
impl From<IconName> for ChatIcon {
fn from(name: IconName) -> Self {
Self::Named(name)
}
}
impl PartialEq for ChatIcon {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Named(a), Self::Named(b)) => a == b,
(Self::Path(a), Self::Path(b)) => a == b,
(Self::Data(a), Self::Data(b)) => a == b,
(Self::Png(a), Self::Png(b)) => a == b,
_ => false,
}
}
}
impl Default for ChatIcon {
fn default() -> Self {
Self::Named(IconName::Sparkles)
}
}
impl fmt::Debug for ChatIcon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Named(name) => f.debug_tuple("Named").field(name).finish(),
Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
Self::Data(bytes) => f
.debug_tuple("Data")
.field(&format_args!("{} bytes", bytes.len()))
.finish(),
Self::Png(image) => f
.debug_tuple("Png")
.field(&format_args!("{} bytes", image.bytes.len()))
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui_kit::SvgRenderer;
const SVG: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" fill="currentColor"/>
</svg>"#;
#[test]
fn inline_svg_icons_render_visible_pixels() {
let icon = ChatIcon::data(SVG);
let ChatIcon::Data(bytes) = &icon else {
panic!("expected inline data, got {icon:?}");
};
let renderer = SvgRenderer::new(Arc::new(gpui_kit::assets::AllAssets));
let parsed = renderer.parse_svg(bytes).expect("parse inline svg");
let image = renderer.render_parsed(&parsed, 2.0).expect("render svg");
let pixels = image.as_bytes(0).expect("frame");
assert!(
pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0),
"inline SVG rasterised to a blank image"
);
}
#[test]
fn a_catalog_icon_converts() {
let icon: ChatIcon = IconName::Bot.into();
assert!(matches!(icon, ChatIcon::Named(IconName::Bot)));
}
#[test]
fn a_raster_mark_has_no_monochrome_form() {
let icon = ChatIcon::png(b"not decoded here");
assert!(matches!(icon, ChatIcon::Png(_)));
assert!(icon.as_icon().is_none());
assert_eq!(icon, ChatIcon::png(b"not decoded here"));
}
}