1use std::{
8 collections::hash_map::DefaultHasher,
9 fmt,
10 hash::{Hash as _, Hasher as _},
11 sync::Arc,
12};
13
14use gpui_kit::{
15 AnyElement, Image, ImageFormat, ImageSource, IntoElement, Pixels, SharedString, Styled as _,
16 assets::IconName, component::Icon, img, px,
17};
18
19#[derive(Clone)]
22pub enum ChatIcon {
23 Named(IconName),
25 Path(SharedString),
27 Data(Arc<[u8]>),
32 Png(Arc<Image>),
37}
38
39impl ChatIcon {
40 pub fn data(bytes: impl AsRef<[u8]>) -> Self {
43 Self::Data(Arc::from(bytes.as_ref()))
44 }
45
46 pub fn path(path: impl Into<SharedString>) -> Self {
48 Self::Path(path.into())
49 }
50
51 pub fn png(bytes: impl AsRef<[u8]>) -> Self {
53 let bytes = bytes.as_ref();
54 let mut hasher = DefaultHasher::new();
55 bytes.hash(&mut hasher);
56 Self::Png(Arc::new(Image {
57 format: ImageFormat::Png,
58 bytes: bytes.to_vec(),
59 id: hasher.finish(),
60 }))
61 }
62
63 pub(crate) fn as_icon(&self) -> Option<Icon> {
68 match self {
69 Self::Named(name) => Some(Icon::new(*name)),
70 Self::Path(path) => Some(Icon::default().path(path.clone())),
71 Self::Data(bytes) => Some(Icon::default().data(bytes)),
72 Self::Png(_) => None,
73 }
74 }
75
76 pub(crate) fn element(&self, size: Option<Pixels>) -> AnyElement {
79 if let Self::Png(image) = self {
80 return img(ImageSource::Image(image.clone()))
81 .size(size.unwrap_or(px(24.)))
82 .into_any_element();
83 }
84
85 let icon = self
86 .as_icon()
87 .unwrap_or_else(|| Icon::new(IconName::Sparkles));
88 match size {
89 Some(size) => icon.size(size).into_any_element(),
90 None => icon.into_any_element(),
91 }
92 }
93}
94
95impl From<IconName> for ChatIcon {
96 fn from(name: IconName) -> Self {
97 Self::Named(name)
98 }
99}
100
101impl PartialEq for ChatIcon {
102 fn eq(&self, other: &Self) -> bool {
103 match (self, other) {
104 (Self::Named(a), Self::Named(b)) => a == b,
105 (Self::Path(a), Self::Path(b)) => a == b,
106 (Self::Data(a), Self::Data(b)) => a == b,
107 (Self::Png(a), Self::Png(b)) => a == b,
108 _ => false,
109 }
110 }
111}
112
113impl Default for ChatIcon {
114 fn default() -> Self {
115 Self::Named(IconName::Sparkles)
116 }
117}
118
119impl fmt::Debug for ChatIcon {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::Named(name) => f.debug_tuple("Named").field(name).finish(),
123 Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
124 Self::Data(bytes) => f
125 .debug_tuple("Data")
126 .field(&format_args!("{} bytes", bytes.len()))
127 .finish(),
128 Self::Png(image) => f
129 .debug_tuple("Png")
130 .field(&format_args!("{} bytes", image.bytes.len()))
131 .finish(),
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use gpui_kit::SvgRenderer;
140
141 const SVG: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
142 <circle cx="8" cy="8" r="7" fill="currentColor"/>
143 </svg>"#;
144
145 #[test]
146 fn inline_svg_icons_render_visible_pixels() {
147 let icon = ChatIcon::data(SVG);
148 let ChatIcon::Data(bytes) = &icon else {
149 panic!("expected inline data, got {icon:?}");
150 };
151 let renderer = SvgRenderer::new(Arc::new(gpui_kit::assets::AllAssets));
152 let parsed = renderer.parse_svg(bytes).expect("parse inline svg");
153 let image = renderer.render_parsed(&parsed, 2.0).expect("render svg");
154 let pixels = image.as_bytes(0).expect("frame");
155 assert!(
156 pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0),
157 "inline SVG rasterised to a blank image"
158 );
159 }
160
161 #[test]
162 fn a_catalog_icon_converts() {
163 let icon: ChatIcon = IconName::Bot.into();
164 assert!(matches!(icon, ChatIcon::Named(IconName::Bot)));
165 }
166
167 #[test]
168 fn a_raster_mark_has_no_monochrome_form() {
169 let icon = ChatIcon::png(b"not decoded here");
170 assert!(matches!(icon, ChatIcon::Png(_)));
171 assert!(icon.as_icon().is_none());
172 assert_eq!(icon, ChatIcon::png(b"not decoded here"));
173 }
174}